給与管理システム新規作成

This commit is contained in:
admin
2026-01-20 11:15:49 +09:00
parent 9dc7cbcb07
commit 8a00de8f03
45 changed files with 8925 additions and 5 deletions

View File

@@ -0,0 +1,605 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.filters {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.filters label {
margin-right: 15px;
}
.payroll-list {
margin-top: 20px;
}
.payroll-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.payroll-item:hover {
background: #f9f9f9;
}
.status-badge {
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: bold;
}
.status-draft {
background: #ffc107;
color: #000;
}
.status-calculated {
background: #17a2b8;
color: #fff;
}
.status-approved {
background: #28a745;
color: #fff;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.form-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.payroll-detail {
margin-top: 20px;
}
.detail-section {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #ddd;
}
.detail-row:last-child {
border-bottom: none;
}
.total-row {
font-weight: bold;
font-size: 1.2em;
color: #007bff;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 月次給与計算</h1>
<div class="filters">
<label>
対象年月:
<input
type="number"
id="filterYear"
style="width: 100px"
placeholder="年"
/>
</label>
<label>
<input
type="number"
id="filterMonth"
style="width: 80px"
min="1"
max="12"
placeholder="月"
/>
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">
新規計算
</button>
</div>
<div id="payrollList" class="payroll-list"></div>
<!-- 給与計算フォーム(モーダル風) -->
<div
id="calculateModal"
style="
display: none;
position: fixed;
top: 50px;
left: 50%;
transform: translateX(-50%);
width: 800px;
max-height: 90vh;
overflow-y: auto;
background: white;
padding: 30px;
border: 2px solid #007bff;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
"
>
<h2>給与計算</h2>
<form id="calculateForm" onsubmit="calculatePayroll(event)">
<div class="form-group">
<label>従業員 *</label>
<select name="employee_id" id="employeeSelect" required>
<option value="">選択してください</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>対象年 *</label>
<input type="number" name="payroll_year" required />
</div>
<div class="form-group">
<label>対象月 *</label>
<input
type="number"
name="payroll_month"
min="1"
max="12"
required
/>
</div>
<div class="form-group">
<label>支給日 *</label>
<input type="date" name="payment_date" required />
</div>
</div>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="form-row">
<div class="form-group">
<label>出勤日数</label>
<input type="number" name="working_days" step="0.5" value="0" />
</div>
<div class="form-group">
<label>勤務時間</label>
<input
type="number"
name="working_hours"
step="0.5"
value="0"
/>
</div>
<div class="form-group">
<label>残業時間</label>
<input
type="number"
name="overtime_hours"
step="0.5"
value="0"
/>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>休日出勤時間</label>
<input
type="number"
name="holiday_hours"
step="0.5"
value="0"
/>
</div>
<div class="form-group">
<label>遅刻時間</label>
<input type="number" name="late_hours" step="0.5" value="0" />
</div>
<div class="form-group">
<label>欠勤日数</label>
<input type="number" name="absent_days" step="0.5" value="0" />
</div>
</div>
</div>
<div class="detail-section">
<h3>その他</h3>
<div class="form-row">
<div class="form-group">
<label>その他手当</label>
<input
type="number"
name="other_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>住民税</label>
<input
type="number"
name="resident_tax"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>その他控除</label>
<input
type="number"
name="other_deduction"
step="0.01"
value="0"
/>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">計算実行</button>
<button
type="button"
class="btn btn-secondary"
onclick="closeCalculateForm()"
>
キャンセル
</button>
</form>
</div>
<!-- 給与明細表示 -->
<div
id="payrollDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<script>
const API_BASE = "http://localhost:8000";
async function loadEmployees() {
const response = await fetch(
`${API_BASE}/payroll/employees/?is_active=true`
);
const employees = await response.json();
const select = document.getElementById("employeeSelect");
select.innerHTML =
'<option value="">選択してください</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
)
.join("");
}
async function loadPayrolls() {
const year = document.getElementById("filterYear").value;
const month = document.getElementById("filterMonth").value;
const params = new URLSearchParams();
if (year) params.append("payroll_year", year);
if (month) params.append("payroll_month", month);
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/?${params}`
);
const payrolls = await response.json();
const html = payrolls
.map(
(p) => `
<div class="payroll-item" onclick="viewPayroll(${
p.payroll_id
})">
<div>
<strong>${p.payroll_year}${
p.payroll_month
}月</strong>
<span class="status-badge status-${
p.status
}">${getStatusLabel(p.status)}</span>
</div>
<div>
従業員ID: ${p.employee_id} | 支給日: ${
p.payment_date
}
</div>
<div>
<strong>差引支給額: ¥${Number(
p.net_payment
).toLocaleString()}</strong>
(総支給: ¥${Number(
p.total_payment
).toLocaleString()} - 控除: ¥${Number(
p.total_deduction
).toLocaleString()})
</div>
</div>
`
)
.join("");
document.getElementById("payrollList").innerHTML =
html || "<p>給与データがありません</p>";
} catch (error) {
alert("給与一覧の取得に失敗しました");
console.error(error);
}
}
function getStatusLabel(status) {
const labels = {
draft: "下書き",
calculated: "計算済み",
approved: "承認済み",
paid: "支払済み",
};
return labels[status] || status;
}
function showCalculateForm() {
document.getElementById("calculateModal").style.display = "block";
loadEmployees();
// デフォルト値設定
const now = new Date();
document.querySelector('[name="payroll_year"]').value =
now.getFullYear();
document.querySelector('[name="payroll_month"]').value =
now.getMonth() + 1;
}
function closeCalculateForm() {
document.getElementById("calculateModal").style.display = "none";
document.getElementById("calculateForm").reset();
}
async function calculatePayroll(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
data[key] = isNaN(value) || value === "" ? value : Number(value);
});
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/calculate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (response.ok) {
const result = await response.json();
alert("給与計算が完了しました");
closeCalculateForm();
viewPayroll(result.payroll_id);
} else {
const error = await response.json();
alert("計算に失敗しました: " + error.detail);
}
} catch (error) {
alert("計算に失敗しました");
console.error(error);
}
}
async function viewPayroll(payrollId) {
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}`
);
const payroll = await response.json();
const html = `
<h2>${payroll.payroll_year}${
payroll.payroll_month
}月 給与明細</h2>
<p>従業員ID: ${payroll.employee_id} | 支給日: ${
payroll.payment_date
}</p>
<p>ステータス: <span class="status-badge status-${
payroll.status
}">${getStatusLabel(payroll.status)}</span></p>
<div class="detail-section">
<h3>勤怠情報</h3>
<div class="detail-row"><span>出勤日数:</span><span>${
payroll.working_days
}日</span></div>
<div class="detail-row"><span>勤務時間:</span><span>${
payroll.working_hours
}時間</span></div>
<div class="detail-row"><span>残業時間:</span><span>${
payroll.overtime_hours
}時間</span></div>
<div class="detail-row"><span>休日出勤:</span><span>${
payroll.holiday_hours
}時間</span></div>
<div class="detail-row"><span>欠勤日数:</span><span>${
payroll.absent_days
}日</span></div>
</div>
<div class="detail-section">
<h3>支給項目</h3>
<div class="detail-row"><span>基本給:</span><span>¥${Number(
payroll.base_salary
).toLocaleString()}</span></div>
<div class="detail-row"><span>残業手当:</span><span>¥${Number(
payroll.overtime_pay
).toLocaleString()}</span></div>
<div class="detail-row"><span>休日手当:</span><span>¥${Number(
payroll.holiday_pay
).toLocaleString()}</span></div>
<div class="detail-row"><span>通勤手当:</span><span>¥${Number(
payroll.commute_allowance
).toLocaleString()}</span></div>
<div class="detail-row"><span>その他手当:</span><span>¥${Number(
payroll.other_allowance
).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
payroll.total_payment
).toLocaleString()}</span></div>
</div>
<div class="detail-section">
<h3>控除項目</h3>
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
payroll.health_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
payroll.pension_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
payroll.employment_insurance
).toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(
payroll.income_tax
).toLocaleString()}</span></div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
payroll.resident_tax
).toLocaleString()}</span></div>
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
payroll.other_deduction
).toLocaleString()}</span></div>
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
payroll.total_deduction
).toLocaleString()}</span></div>
</div>
<div class="detail-section" style="background:#e7f3ff;">
<div class="detail-row total-row" style="font-size:1.5em;">
<span>差引支給額:</span>
<span>¥${Number(
payroll.net_payment
).toLocaleString()}</span>
</div>
</div>
${
payroll.status === "calculated"
? `
<button class="btn btn-success" onclick="approvePayroll(${payroll.payroll_id})">承認</button>
<button class="btn btn-primary" onclick="recalculatePayroll(${payroll.payroll_id})">再計算</button>
`
: ""
}
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
`;
document.getElementById("payrollDetail").innerHTML = html;
document.getElementById("payrollDetail").style.display = "block";
document
.getElementById("payrollDetail")
.scrollIntoView({ behavior: "smooth" });
} catch (error) {
alert("給与明細の取得に失敗しました");
console.error(error);
}
}
async function approvePayroll(payrollId) {
if (!confirm("この給与を承認しますか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/approve`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ approved_by: "admin" }),
}
);
if (response.ok) {
alert("承認しました");
viewPayroll(payrollId);
loadPayrolls();
} else {
const error = await response.json();
alert("承認に失敗しました: " + error.detail);
}
} catch (error) {
alert("承認に失敗しました");
console.error(error);
}
}
async function recalculatePayroll(payrollId) {
if (!confirm("給与を再計算しますか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/recalculate`,
{
method: "POST",
}
);
if (response.ok) {
alert("再計算が完了しました");
viewPayroll(payrollId);
} else {
const error = await response.json();
alert("再計算に失敗しました: " + error.detail);
}
} catch (error) {
alert("再計算に失敗しました");
console.error(error);
}
}
// 初期化
const now = new Date();
document.getElementById("filterYear").value = now.getFullYear();
document.getElementById("filterMonth").value = now.getMonth() + 1;
loadPayrolls();
</script>
</body>
</html>

View File

@@ -0,0 +1,371 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 従業員管理</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.nav-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #ddd;
}
.nav-tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: #f0f0f0;
border-radius: 5px 5px 0 0;
}
.nav-tab.active {
background: #007bff;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.employee-list {
margin-top: 20px;
}
.employee-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.employee-item:hover {
background: #f9f9f9;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
.dependent-list {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.dependent-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
background: white;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 従業員管理</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('list')">
従業員一覧
</button>
<button class="nav-tab" onclick="showTab('create')">新規登録</button>
<button
class="nav-tab"
onclick="showTab('detail')"
id="detailTab"
style="display: none"
>
従業員詳細
</button>
</div>
<!-- 従業員一覧タブ -->
<div id="tab-list" class="tab-content active">
<div class="filters">
<label>
<input
type="checkbox"
id="filterActive"
checked
onchange="loadEmployees()"
/>
在職中のみ表示
</label>
</div>
<div id="employeeList" class="employee-list"></div>
</div>
<!-- 新規登録タブ -->
<div id="tab-create" class="tab-content">
<h2>従業員の新規登録</h2>
<form id="createForm" onsubmit="createEmployee(event)">
<div class="form-group">
<label>従業員コード *</label>
<input type="text" name="employee_code" required />
</div>
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" required />
</div>
<div class="form-group">
<label>フリガナ</label>
<input type="text" name="name_kana" />
</div>
<div class="form-group">
<label>メールアドレス</label>
<input type="email" name="email" />
</div>
<div class="form-group">
<label>電話番号</label>
<input type="tel" name="phone" />
</div>
<div class="form-group">
<label>入社日 *</label>
<input type="date" name="hire_date" required />
</div>
<div class="form-group">
<label>銀行名</label>
<input type="text" name="bank_name" />
</div>
<div class="form-group">
<label>支店名</label>
<input type="text" name="bank_branch" />
</div>
<div class="form-group">
<label>口座番号</label>
<input type="text" name="bank_account_number" />
</div>
<div class="form-group">
<label>口座種別</label>
<select name="bank_account_type">
<option value="">選択してください</option>
<option value="普通">普通</option>
<option value="当座">当座</option>
</select>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="showTab('list')"
>
キャンセル
</button>
</form>
</div>
<!-- 従業員詳細タブ -->
<div id="tab-detail" class="tab-content">
<div id="employeeDetail"></div>
</div>
</div>
<script>
const API_BASE = "http://localhost:8000";
let currentEmployee = null;
function showTab(tabName) {
document
.querySelectorAll(".nav-tab")
.forEach((tab) => tab.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
if (tabName === "list") {
document.querySelector(".nav-tab").classList.add("active");
document.getElementById("tab-list").classList.add("active");
loadEmployees();
} else if (tabName === "create") {
document.querySelectorAll(".nav-tab")[1].classList.add("active");
document.getElementById("tab-create").classList.add("active");
} else if (tabName === "detail") {
document.getElementById("detailTab").classList.add("active");
document.getElementById("tab-detail").classList.add("active");
}
}
async function loadEmployees() {
const isActive = document.getElementById("filterActive").checked;
const url = `${API_BASE}/payroll/employees/${
isActive ? "?is_active=true" : ""
}`;
try {
const response = await fetch(url);
const employees = await response.json();
const html = employees
.map(
(emp) => `
<div class="employee-item" onclick="viewEmployee(${
emp.employee_id
})">
<strong>${emp.employee_code}</strong> - ${emp.name}
${emp.name_kana ? `(${emp.name_kana})` : ""}
<br>
<small>入社日: ${emp.hire_date} ${
emp.termination_date ? ` | 退職日: ${emp.termination_date}` : ""
}</small>
</div>
`
)
.join("");
document.getElementById("employeeList").innerHTML =
html || "<p>従業員が登録されていません</p>";
} catch (error) {
alert("従業員一覧の取得に失敗しました");
console.error(error);
}
}
async function createEmployee(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// 空の値を除外
Object.keys(data).forEach((key) => {
if (data[key] === "") {
delete data[key];
}
});
try {
const response = await fetch(`${API_BASE}/payroll/employees/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("従業員を登録しました");
form.reset();
showTab("list");
} else {
const error = await response.json();
alert("登録に失敗しました: " + error.detail);
}
} catch (error) {
alert("登録に失敗しました");
console.error(error);
}
}
async function viewEmployee(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}`
);
const employee = await response.json();
currentEmployee = employee;
const html = `
<h2>${employee.name} (${employee.employee_code})</h2>
<p><strong>フリガナ:</strong> ${
employee.name_kana || "-"
}</p>
<p><strong>メール:</strong> ${employee.email || "-"}</p>
<p><strong>電話:</strong> ${employee.phone || "-"}</p>
<p><strong>入社日:</strong> ${employee.hire_date}</p>
<p><strong>退職日:</strong> ${
employee.termination_date || "-"
}</p>
<p><strong>銀行:</strong> ${employee.bank_name || "-"} ${
employee.bank_branch || ""
}</p>
<p><strong>口座:</strong> ${
employee.bank_account_type || ""
} ${employee.bank_account_number || "-"}</p>
<p><strong>備考:</strong> ${employee.notes || "-"}</p>
<div class="dependent-list">
<h3>扶養家族</h3>
${
employee.dependents
.map(
(dep) => `
<div class="dependent-item">
<strong>${dep.name}</strong> (${
dep.relationship
})
${
dep.birth_date
? ` - 生年月日: ${dep.birth_date}`
: ""
}
${
dep.is_spouse
? ' <span style="color:blue;">[配偶者]</span>'
: ""
}
${
dep.is_disabled
? ' <span style="color:red;">[障害者]</span>'
: ""
}
<br><small>有効期間: ${dep.valid_from} ${
dep.valid_to || "現在"
}</small>
</div>
`
)
.join("") || "<p>扶養家族が登録されていません</p>"
}
</div>
<button class="btn btn-secondary" onclick="showTab('list')">戻る</button>
`;
document.getElementById("employeeDetail").innerHTML = html;
document.getElementById("detailTab").style.display = "block";
showTab("detail");
} catch (error) {
alert("従業員情報の取得に失敗しました");
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

View File

@@ -0,0 +1,378 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 給与設定</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.employee-select {
margin-bottom: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn {
padding: 10px 20px;
margin-right: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table th,
table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
table th {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 給与設定</h1>
<div class="employee-select">
<label><strong>従業員を選択:</strong></label>
<select id="employeeSelect" onchange="loadSalarySettings()">
<option value="">従業員を選択してください</option>
</select>
</div>
<div id="settingsContent" style="display: none">
<h2>給与設定履歴</h2>
<button class="btn btn-primary" onclick="showAddForm()">
新規設定追加
</button>
<div id="settingsList"></div>
<div
id="addForm"
style="
display: none;
margin-top: 20px;
padding: 20px;
border: 2px solid #007bff;
border-radius: 5px;
"
>
<h3>給与設定の追加</h3>
<form onsubmit="saveSalarySetting(event)">
<div class="form-group">
<label>基本給 (円) *</label>
<input type="number" name="base_salary" step="0.01" required />
</div>
<div class="form-group">
<label>時給 (円)</label>
<input type="number" name="hourly_rate" step="0.01" />
<small>時給制の場合に入力してください</small>
</div>
<div class="form-group">
<label>雇用形態 *</label>
<select name="employment_type" required>
<option value="">選択してください</option>
<option value="正社員">正社員</option>
<option value="契約社員">契約社員</option>
<option value="パート">パート</option>
<option value="アルバイト">アルバイト</option>
</select>
</div>
<div class="form-group">
<label>支給形態 *</label>
<select name="payment_type" required>
<option value="">選択してください</option>
<option value="月給">月給</option>
<option value="時給">時給</option>
<option value="日給">日給</option>
</select>
</div>
<div class="form-group">
<label>通勤手当 (円)</label>
<input
type="number"
name="commute_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>その他手当 (円)</label>
<input
type="number"
name="other_allowance"
step="0.01"
value="0"
/>
</div>
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" required />
</div>
<div class="form-group">
<label>適用終了日</label>
<input type="date" name="valid_to" />
<small>通常は空欄のままにしてください</small>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button
type="button"
class="btn btn-secondary"
onclick="hideAddForm()"
>
キャンセル
</button>
</form>
</div>
<div
id="currentSetting"
style="
margin-top: 30px;
padding: 20px;
background: #e7f3ff;
border-radius: 5px;
"
>
<h3>現在の給与設定</h3>
<div id="currentSettingContent"></div>
</div>
</div>
</div>
<script>
const API_BASE = "http://localhost:8000";
let currentEmployeeId = null;
async function loadEmployees() {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/?is_active=true`
);
const employees = await response.json();
const select = document.getElementById("employeeSelect");
select.innerHTML =
'<option value="">従業員を選択してください</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
)
.join("");
} catch (error) {
alert("従業員一覧の取得に失敗しました");
console.error(error);
}
}
async function loadSalarySettings() {
const employeeId = document.getElementById("employeeSelect").value;
if (!employeeId) {
document.getElementById("settingsContent").style.display = "none";
return;
}
currentEmployeeId = employeeId;
document.getElementById("settingsContent").style.display = "block";
try {
// 給与設定履歴を取得
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${employeeId}`
);
const settings = await response.json();
const html = `
<table>
<thead>
<tr>
<th>適用期間</th>
<th>基本給</th>
<th>時給</th>
<th>雇用形態</th>
<th>支給形態</th>
<th>通勤手当</th>
<th>その他手当</th>
</tr>
</thead>
<tbody>
${settings
.map(
(s) => `
<tr>
<td>${s.valid_from} ${
s.valid_to || "現在"
}</td>
<td>¥${Number(
s.base_salary
).toLocaleString()}</td>
<td>${
s.hourly_rate
? "¥" +
Number(s.hourly_rate).toLocaleString()
: "-"
}</td>
<td>${s.employment_type}</td>
<td>${s.payment_type}</td>
<td>¥${Number(
s.commute_allowance
).toLocaleString()}</td>
<td>¥${Number(
s.other_allowance
).toLocaleString()}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("settingsList").innerHTML = html;
// 現在の設定を取得
loadCurrentSetting(employeeId);
} catch (error) {
alert("給与設定の取得に失敗しました");
console.error(error);
}
}
async function loadCurrentSetting(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${employeeId}/current`
);
if (response.ok) {
const setting = await response.json();
const html = `
<div style="display:grid; grid-template-columns: repeat(2, 1fr); gap:15px;">
<div><strong>基本給:</strong> ¥${Number(
setting.base_salary
).toLocaleString()}</div>
<div><strong>時給:</strong> ${
setting.hourly_rate
? "¥" +
Number(setting.hourly_rate).toLocaleString()
: "-"
}</div>
<div><strong>雇用形態:</strong> ${
setting.employment_type
}</div>
<div><strong>支給形態:</strong> ${
setting.payment_type
}</div>
<div><strong>通勤手当:</strong> ¥${Number(
setting.commute_allowance
).toLocaleString()}</div>
<div><strong>その他手当:</strong> ¥${Number(
setting.other_allowance
).toLocaleString()}</div>
<div><strong>適用期間:</strong> ${
setting.valid_from
} ${setting.valid_to || "現在"}</div>
</div>
`;
document.getElementById("currentSettingContent").innerHTML = html;
} else {
document.getElementById("currentSettingContent").innerHTML =
"<p>給与設定が登録されていません</p>";
}
} catch (error) {
document.getElementById("currentSettingContent").innerHTML =
"<p>給与設定の取得に失敗しました</p>";
}
}
function showAddForm() {
document.getElementById("addForm").style.display = "block";
// デフォルト値設定
document.querySelector('[name="valid_from"]').value = new Date()
.toISOString()
.split("T")[0];
}
function hideAddForm() {
document.getElementById("addForm").style.display = "none";
document.querySelector("#addForm form").reset();
}
async function saveSalarySetting(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
data.employee_id = parseInt(currentEmployeeId);
data.base_salary = parseFloat(data.base_salary) || 0;
data.commute_allowance = parseFloat(data.commute_allowance) || 0;
data.other_allowance = parseFloat(data.other_allowance) || 0;
if (data.hourly_rate) {
data.hourly_rate = parseFloat(data.hourly_rate);
} else {
delete data.hourly_rate;
}
if (!data.valid_to) {
delete data.valid_to;
}
try {
const response = await fetch(`${API_BASE}/payroll/settings/salary`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("給与設定を保存しました");
hideAddForm();
loadSalarySettings();
} else {
const error = await response.json();
alert("保存に失敗しました: " + error.detail);
}
} catch (error) {
alert("保存に失敗しました");
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

121
frontend/payroll.html Normal file
View File

@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理システム</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.menu-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.menu-card {
padding: 30px;
border: 2px solid #007bff;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.menu-card:hover {
background: #007bff;
color: white;
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 123, 255, 0.3);
}
.menu-card h2 {
margin: 0 0 15px 0;
font-size: 1.5em;
}
.menu-card p {
margin: 0;
opacity: 0.8;
}
.menu-card:hover p {
opacity: 1;
}
.icon {
font-size: 3em;
margin-bottom: 15px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #007bff;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<a href="../index.html" class="back-link">← メインメニューに戻る</a>
<h1>給与管理システム</h1>
<p>従業員の給与計算、扶養管理、保険料・税金の管理を行います</p>
<div class="menu-grid">
<div class="menu-card" onclick="location.href='payroll-employees.html'">
<div class="icon">👥</div>
<h2>従業員管理</h2>
<p>従業員の基本情報、扶養家族情報を管理します</p>
</div>
<div
class="menu-card"
onclick="location.href='payroll-salary-settings.html'"
>
<div class="icon">💰</div>
<h2>給与設定</h2>
<p>各従業員の給与設定、基本給、手当などを管理します</p>
</div>
<div
class="menu-card"
onclick="location.href='payroll-calculation.html'"
>
<div class="icon">🧮</div>
<h2>月次給与計算</h2>
<p>毎月の給与計算、給与明細の作成・承認を行います</p>
</div>
<div class="menu-card" onclick="location.href='payroll-settings.html'">
<div class="icon">⚙️</div>
<h2>税率・保険料設定</h2>
<p>社会保険料率、所得税率表の管理を行います</p>
</div>
</div>
<div
style="
margin-top: 50px;
padding: 20px;
background: #f0f8ff;
border-radius: 5px;
"
>
<h3>📋 給与システムの使い方</h3>
<ol>
<li>
<strong>従業員管理</strong
>で従業員を登録し、扶養家族情報を入力します
</li>
<li><strong>給与設定</strong>で各従業員の基本給や手当を設定します</li>
<li>
<strong>税率・保険料設定</strong
>で社会保険料率や所得税率表を確認・更新します
</li>
<li>
<strong>月次給与計算</strong
>で勤怠情報を入力し、給与を計算・承認します
</li>
</ol>
</div>
</div>
</body>
</html>