1452 lines
53 KiB
HTML
1452 lines
53 KiB
HTML
<!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;
|
||
}
|
||
/* 明細を中央に寄せ、横幅を狭める(読みやすくするため) */
|
||
.payroll-detail {
|
||
max-width: 760px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
}
|
||
.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;
|
||
}
|
||
.back-link {
|
||
display: inline-block;
|
||
margin-bottom: 20px;
|
||
color: #007bff;
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
}
|
||
.back-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
/* タブスタイル */
|
||
.nav-tabs-calc {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
border-bottom: 2px solid #ddd;
|
||
}
|
||
.nav-tab-calc {
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
border: none;
|
||
background: #f0f0f0;
|
||
border-radius: 5px 5px 0 0;
|
||
}
|
||
.nav-tab-calc.active {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
.tab-content-calc {
|
||
display: none;
|
||
}
|
||
.tab-content-calc.active {
|
||
display: block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<a href="payroll.html" class="back-link">← 給与管理メニューに戻る</a>
|
||
|
||
<h1>給与管理 - 給与・賞与計算</h1>
|
||
|
||
<div class="nav-tabs-calc">
|
||
<button class="nav-tab-calc active" onclick="switchTab('salary')">
|
||
給与計算
|
||
</button>
|
||
<button class="nav-tab-calc" onclick="switchTab('bonus')">
|
||
賞与計算
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 給与計算タブ -->
|
||
<div id="tab-salary" class="tab-content-calc active">
|
||
<div class="filters">
|
||
<label>
|
||
対象年月:
|
||
<input
|
||
type="month"
|
||
id="filterYearMonth"
|
||
style="width: 160px"
|
||
placeholder="YYYY-MM"
|
||
/>
|
||
</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="text"
|
||
name="payroll_year"
|
||
inputmode="numeric"
|
||
pattern="\d{4}"
|
||
maxlength="4"
|
||
oninput="sanitizeDigits(this,4)"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>対象月 *</label>
|
||
<select name="payroll_month" required>
|
||
<option value="">選択してください</option>
|
||
<option value="1">1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
<option value="5">5</option>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>支給日 *</label>
|
||
<input
|
||
type="date"
|
||
name="payment_date"
|
||
required
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</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>
|
||
|
||
<!-- 賞与計算タブ -->
|
||
<div id="tab-bonus" class="tab-content-calc">
|
||
<div class="filters">
|
||
<label>
|
||
対象年月:
|
||
<input
|
||
type="month"
|
||
id="filterBonusYearMonth"
|
||
style="width: 160px"
|
||
placeholder="YYYY-MM"
|
||
/>
|
||
</label>
|
||
<button class="btn btn-primary" onclick="loadBonus()">検索</button>
|
||
<button class="btn btn-success" onclick="showBonusForm()">
|
||
新規計算
|
||
</button>
|
||
</div>
|
||
|
||
<div id="bonusList" class="payroll-list"></div>
|
||
|
||
<!-- 賞与計算フォーム -->
|
||
<div
|
||
id="bonusModal"
|
||
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="bonusForm" onsubmit="calculateBonus(event)">
|
||
<div class="form-group">
|
||
<label>従業員 *</label>
|
||
<select name="employee_id" id="bonusEmployeeSelect" required>
|
||
<option value="">選択してください</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>対象年 *</label>
|
||
<input
|
||
type="text"
|
||
name="bonus_year"
|
||
inputmode="numeric"
|
||
pattern="\d{4}"
|
||
maxlength="4"
|
||
oninput="sanitizeDigits(this,4)"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>対象月 *</label>
|
||
<select name="bonus_month" required>
|
||
<option value="">選択してください</option>
|
||
<option value="1">1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
<option value="5">5</option>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>賞与種別 *</label>
|
||
<select name="bonus_type" required>
|
||
<option value="">選択してください</option>
|
||
<option value="夏季賞与">夏季賞与</option>
|
||
<option value="冬季賞与">冬季賞与</option>
|
||
<option value="決算賞与">決算賞与</option>
|
||
<option value="その他">その他</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>支給日 *</label>
|
||
<input
|
||
type="date"
|
||
name="payment_date"
|
||
required
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>基本賞与額</label>
|
||
<input type="number" name="base_bonus" step="0.01" value="0" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>業績賞与</label>
|
||
<input
|
||
type="number"
|
||
name="performance_bonus"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>その他賞与</label>
|
||
<input type="number" name="other_bonus" 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>
|
||
|
||
<button type="submit" class="btn btn-primary">計算実行</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="closeBonusForm()"
|
||
>
|
||
キャンセル
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 賞与明細表示 -->
|
||
<div
|
||
id="bonusDetail"
|
||
class="payroll-detail"
|
||
style="display: none"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Use relative API paths so frontend works when served via nginx on the NAS.
|
||
// The nginx service will proxy API requests to the backend container.
|
||
const API_BASE = "";
|
||
let employeeMap = {};
|
||
|
||
// sanitize input to digits only and limit length
|
||
function sanitizeDigits(el, maxLen) {
|
||
if (!el) return;
|
||
// remove non-digits
|
||
el.value = el.value.replace(/\D/g, "");
|
||
if (maxLen && el.value.length > maxLen)
|
||
el.value = el.value.slice(0, maxLen);
|
||
}
|
||
|
||
function validateYearMonth(yearStr, monthStr) {
|
||
const now = new Date();
|
||
const currentYear = now.getFullYear();
|
||
if (!/^[0-9]{4}$/.test(String(yearStr)))
|
||
return {
|
||
ok: false,
|
||
message: "対象年は4桁の数字で入力してください(例: 2026)",
|
||
};
|
||
const year = Number(yearStr);
|
||
if (year < 1900 || year > currentYear + 1)
|
||
return {
|
||
ok: false,
|
||
message: `対象年は1900〜${currentYear + 1}の範囲で入力してください`,
|
||
};
|
||
if (!/^[0-9]{1,2}$/.test(String(monthStr)))
|
||
return {
|
||
ok: false,
|
||
message: "対象月は数字のみで入力してください(1〜12)",
|
||
};
|
||
const month = Number(monthStr);
|
||
if (month < 1 || month > 12)
|
||
return {
|
||
ok: false,
|
||
message: "対象月は1〜12の範囲で入力してください",
|
||
};
|
||
return { ok: true, year, month };
|
||
}
|
||
|
||
async function loadEmployees() {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/employees/?is_active=true`
|
||
);
|
||
const employees = await response.json();
|
||
|
||
// マップを作成して再利用できるようにする
|
||
employeeMap = {};
|
||
employees.forEach((emp) => {
|
||
employeeMap[emp.employee_id] = emp;
|
||
});
|
||
|
||
// 給与フォームと賞与フォーム両方のセレクトボックスを更新
|
||
const optionsHtml =
|
||
'<option value="">選択してください</option>' +
|
||
employees
|
||
.map(
|
||
(emp) =>
|
||
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
|
||
)
|
||
.join("");
|
||
|
||
const salarySelect = document.getElementById("employeeSelect");
|
||
if (salarySelect) salarySelect.innerHTML = optionsHtml;
|
||
|
||
const bonusSelect = document.getElementById("bonusEmployeeSelect");
|
||
if (bonusSelect) bonusSelect.innerHTML = optionsHtml;
|
||
|
||
return employees;
|
||
}
|
||
|
||
async function loadPayrolls() {
|
||
const ym = document.getElementById("filterYearMonth").value;
|
||
let year = null;
|
||
let month = null;
|
||
if (ym) {
|
||
const parts = ym.split("-");
|
||
if (parts.length === 2) {
|
||
year = Number(parts[0]);
|
||
month = Number(parts[1]);
|
||
}
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
if (year) params.append("payroll_year", year);
|
||
if (month) params.append("payroll_month", month);
|
||
// employeeMap が空なら読み込む
|
||
if (!employeeMap || Object.keys(employeeMap).length === 0) {
|
||
await loadEmployees();
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/?${params}`
|
||
);
|
||
const payrolls = await response.json();
|
||
const html = payrolls
|
||
.map((p) => {
|
||
const emp = employeeMap[p.employee_id];
|
||
const empLabel = emp
|
||
? `従業員ID: ${p.employee_id} | ${emp.employee_code} - ${emp.name}`
|
||
: `従業員ID: ${p.employee_id}`;
|
||
|
||
return `
|
||
<div class="payroll-item" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div style="flex: 1; cursor: pointer;" 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>
|
||
${empLabel} | 支給日: ${p.payment_date}
|
||
</div>
|
||
<div>
|
||
<strong>差引支給額: ¥${Number(
|
||
p.net_payment
|
||
).toLocaleString()}</strong>
|
||
(総支給: ¥${Number(
|
||
p.total_payment
|
||
).toLocaleString()} - 控除: ¥${Number(
|
||
p.total_deduction
|
||
).toLocaleString()})
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deletePayroll(${
|
||
p.payroll_id
|
||
}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; background-color: #dc3545; color: #fff; border: none;">
|
||
削除
|
||
</button>
|
||
</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);
|
||
// validate year/month
|
||
const yearStr = formData.get("payroll_year");
|
||
const monthStr = formData.get("payroll_month");
|
||
const valid = validateYearMonth(yearStr, monthStr);
|
||
if (!valid.ok) {
|
||
alert(valid.message);
|
||
return;
|
||
}
|
||
|
||
const data = {};
|
||
formData.forEach((value, key) => {
|
||
// convert numeric-like fields to Number, keep others as-is
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
// ensure payroll_year/month are numbers
|
||
data.payroll_year = valid.year;
|
||
data.payroll_month = valid.month;
|
||
|
||
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();
|
||
const errorMessage =
|
||
typeof error.detail === "string"
|
||
? error.detail
|
||
: JSON.stringify(error.detail || error);
|
||
alert("計算に失敗しました:\n\n" + errorMessage);
|
||
}
|
||
} 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 empResponse = await fetch(
|
||
`${API_BASE}/payroll/employees/${payroll.employee_id}`
|
||
);
|
||
const employee = await empResponse.json();
|
||
|
||
// 源泉税控除対象人数を計算(16歳以上19歳未満の一般扶養親族 + 19歳以上23歳未満の特定扶養親族)
|
||
const dependentCount = employee.dependents
|
||
? employee.dependents.filter((d) => {
|
||
// 有効期間チェック
|
||
if (d.valid_to && new Date(d.valid_to) < new Date())
|
||
return false;
|
||
|
||
// 生年月日から年齢を計算(その年の12月31日時点)
|
||
if (!d.birth_date) return false;
|
||
const birthDate = new Date(d.birth_date);
|
||
const targetYear = payroll.payroll_year;
|
||
const yearEnd = new Date(targetYear, 11, 31); // 12月31日
|
||
let age = yearEnd.getFullYear() - birthDate.getFullYear();
|
||
const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
|
||
if (
|
||
monthDiff < 0 ||
|
||
(monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())
|
||
) {
|
||
age--;
|
||
}
|
||
|
||
// 16歳以上が源泉税控除対象(16-18歳は一般扶養、19-22歳は特定扶養)
|
||
return age >= 16;
|
||
}).length
|
||
: 0;
|
||
|
||
const html = `
|
||
<h2>${payroll.payroll_year}年${
|
||
payroll.payroll_month
|
||
}月 給与明細</h2>
|
||
<p><strong>従業員:</strong> ${employee.employee_code} - ${
|
||
employee.name
|
||
} | <strong>支給日:</strong> ${
|
||
payroll.payment_date
|
||
} | <strong>甲欄扶養親族人数(源泉税控除対象):</strong> ${dependentCount}人</p>
|
||
<p>ステータス: <span class="status-badge status-${
|
||
payroll.status
|
||
}">${getStatusLabel(payroll.status)}</span></p>
|
||
|
||
<div class="detail-section">
|
||
<h3>勤怠情報 ${
|
||
payroll.status === "calculated"
|
||
? '<button class="btn btn-primary" onclick="editPayroll(' +
|
||
payroll.payroll_id +
|
||
')" style="padding: 5px 10px; font-size: 12px; float: right;">編集</button>'
|
||
: ""
|
||
}</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 class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||
payroll.health_insurance
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>介護保険:</span><span>¥${Number(
|
||
payroll.care_insurance || 0
|
||
).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 style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${Number(
|
||
Number(payroll.health_insurance || 0) +
|
||
Number(payroll.care_insurance || 0) +
|
||
Number(payroll.pension_insurance || 0) +
|
||
Number(payroll.employment_insurance || 0)
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row">
|
||
<span>所得税 <small style="color:#666;">(甲欄${dependentCount}人)</small>:</span>
|
||
<span>¥${Number(
|
||
payroll.income_tax
|
||
).toLocaleString()}</span>
|
||
</div>
|
||
<div style="padding: 5px 10px; background: #f0f8ff; border-radius: 3px; margin: 5px 0; font-size: 12px; color: #666;">
|
||
<small>
|
||
※課税対象額から源泉徴収税額表(甲欄)を参照して計算<br>
|
||
扶養親族${dependentCount}人(16歳以上が対象)
|
||
</small>
|
||
</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 editPayroll(payrollId) {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`
|
||
);
|
||
const payroll = await response.json();
|
||
|
||
// 編集フォームを表示
|
||
const html = `
|
||
<div style="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="editPayrollForm" onsubmit="savePayrollEdit(event, ${payrollId})">
|
||
<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="${payroll.working_days}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>勤務時間</label>
|
||
<input type="number" name="working_hours" step="0.5" value="${payroll.working_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>残業時間</label>
|
||
<input type="number" name="overtime_hours" step="0.5" value="${payroll.overtime_hours}" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>休日出勤時間</label>
|
||
<input type="number" name="holiday_hours" step="0.5" value="${payroll.holiday_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>遅刻時間</label>
|
||
<input type="number" name="late_hours" step="0.5" value="${payroll.late_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>欠勤日数</label>
|
||
<input type="number" name="absent_days" step="0.5" value="${payroll.absent_days}" />
|
||
</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="${payroll.other_allowance}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>住民税</label>
|
||
<input type="number" name="resident_tax" step="0.01" value="${payroll.resident_tax}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他控除</label>
|
||
<input type="number" name="other_deduction" step="0.01" value="${payroll.other_deduction}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">保存して再計算</button>
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button>
|
||
</form>
|
||
</div>
|
||
`;
|
||
|
||
const editDiv = document.createElement("div");
|
||
editDiv.innerHTML = html;
|
||
document.body.appendChild(editDiv);
|
||
} catch (error) {
|
||
alert("給与データの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function savePayrollEdit(event, payrollId) {
|
||
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/${payrollId}`,
|
||
{
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("給与データを更新しました");
|
||
form.closest("div").remove();
|
||
|
||
// 再計算
|
||
await recalculatePayroll(payrollId);
|
||
} else {
|
||
const error = await response.json();
|
||
alert("更新に失敗しました: " + error.detail);
|
||
}
|
||
} 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);
|
||
}
|
||
}
|
||
|
||
async function deletePayroll(payrollId) {
|
||
if (
|
||
!confirm("この給与データを削除しますか?\nこの操作は取り消せません。")
|
||
)
|
||
return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||
{
|
||
method: "DELETE",
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("給与データを削除しました");
|
||
document.getElementById("payrollDetail").style.display = "none";
|
||
loadPayrolls();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(
|
||
"削除に失敗しました: " + JSON.stringify(error.detail || error)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
alert("削除に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// 初期化: 月ピッカーにセットして一覧を読み込む
|
||
const now = new Date();
|
||
const ym =
|
||
now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
||
document.getElementById("filterYearMonth").value = ym;
|
||
document.getElementById("filterBonusYearMonth").value = ym;
|
||
loadPayrolls();
|
||
|
||
// =====================================================
|
||
// タブ切り替え機能
|
||
// =====================================================
|
||
let currentTab = "salary";
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
|
||
// タブボタンの表示切り替え
|
||
document
|
||
.querySelectorAll(".nav-tab-calc")
|
||
.forEach((btn) => btn.classList.remove("active"));
|
||
event.target.classList.add("active");
|
||
|
||
// タブコンテンツの表示切り替え
|
||
document.getElementById("tab-salary").classList.remove("active");
|
||
document.getElementById("tab-bonus").classList.remove("active");
|
||
document.getElementById("tab-" + tab).classList.add("active");
|
||
|
||
// 初回表示時に一覧を読み込む
|
||
if (tab === "bonus") {
|
||
loadBonus();
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 賞与計算関連
|
||
// =====================================================
|
||
async function loadBonus() {
|
||
const ym = document.getElementById("filterBonusYearMonth").value;
|
||
let year = null;
|
||
let month = null;
|
||
if (ym) {
|
||
const parts = ym.split("-");
|
||
if (parts.length === 2) {
|
||
year = Number(parts[0]);
|
||
month = Number(parts[1]);
|
||
}
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
if (year) params.append("bonus_year", year);
|
||
if (month) params.append("bonus_month", month);
|
||
|
||
// employeeMapが空なら読み込む
|
||
if (!employeeMap || Object.keys(employeeMap).length === 0) {
|
||
await loadEmployees();
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/?${params}`);
|
||
const bonuses = await response.json();
|
||
|
||
const html = bonuses
|
||
.map((b) => {
|
||
const emp = employeeMap[b.employee_id];
|
||
const empLabel = emp
|
||
? `${emp.employee_code} - ${emp.name}`
|
||
: `従業員: 未登録`;
|
||
|
||
return `
|
||
<div class="payroll-item" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div style="flex: 1; cursor: pointer;" onclick="viewBonus(${
|
||
b.bonus_id
|
||
})">
|
||
<div>
|
||
<strong>${b.bonus_year}年${
|
||
b.bonus_month
|
||
}月</strong>
|
||
<span class="status-badge status-calculated">${
|
||
b.bonus_type
|
||
}</span>
|
||
</div>
|
||
<div>
|
||
${empLabel} | 支給日: ${b.payment_date}
|
||
</div>
|
||
<div>
|
||
<strong>差引支給額: ¥${Number(
|
||
b.net_bonus
|
||
).toLocaleString()}</strong>
|
||
(総支給: ¥${Number(
|
||
b.total_bonus
|
||
).toLocaleString()} - 控除: ¥${Number(
|
||
b.total_deduction || 0
|
||
).toLocaleString()})
|
||
</div>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deleteBonus(${
|
||
b.bonus_id
|
||
}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; background-color: #dc3545; color: #fff; border: none;">
|
||
削除
|
||
</button>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
document.getElementById("bonusList").innerHTML =
|
||
html || "<p>賞与データがありません</p>";
|
||
} catch (error) {
|
||
alert("賞与一覧の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function showBonusForm() {
|
||
document.getElementById("bonusModal").style.display = "block";
|
||
loadEmployees();
|
||
|
||
// デフォルト値設定
|
||
const now = new Date();
|
||
document.querySelector("#bonusForm [name='bonus_year']").value =
|
||
now.getFullYear();
|
||
document.querySelector("#bonusForm [name='bonus_month']").value =
|
||
now.getMonth() + 1;
|
||
}
|
||
|
||
function closeBonusForm() {
|
||
document.getElementById("bonusModal").style.display = "none";
|
||
document.getElementById("bonusForm").reset();
|
||
}
|
||
|
||
async function calculateBonus(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
// validate year/month
|
||
const yearStr = formData.get("bonus_year");
|
||
const monthStr = formData.get("bonus_month");
|
||
const valid = validateYearMonth(yearStr, monthStr);
|
||
if (!valid.ok) {
|
||
alert(valid.message);
|
||
return;
|
||
}
|
||
|
||
const data = {};
|
||
formData.forEach((value, key) => {
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
data.bonus_year = valid.year;
|
||
data.bonus_month = valid.month;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/calculate`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
alert("賞与計算が完了しました");
|
||
closeBonusForm();
|
||
viewBonus(result.bonus_id);
|
||
} else {
|
||
const error = await response.json();
|
||
const errorMessage =
|
||
typeof error.detail === "string"
|
||
? error.detail
|
||
: JSON.stringify(error.detail || error);
|
||
alert("計算に失敗しました:\n\n" + errorMessage);
|
||
}
|
||
} catch (error) {
|
||
alert("計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function viewBonus(bonusId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`);
|
||
const bonus = await response.json();
|
||
|
||
// 従業員情報を取得
|
||
const empResponse = await fetch(
|
||
`${API_BASE}/payroll/employees/${bonus.employee_id}`
|
||
);
|
||
const employee = await empResponse.json();
|
||
|
||
const html = `
|
||
<h2>${bonus.bonus_year}年${
|
||
bonus.bonus_month
|
||
}月 賞与明細</h2>
|
||
<p><strong>従業員:</strong> ${employee.employee_code} - ${
|
||
employee.name
|
||
} | <strong>支給日:</strong> ${
|
||
bonus.payment_date
|
||
} | <strong>賞与種別:</strong> ${bonus.bonus_type}</p>
|
||
|
||
<div class="detail-section">
|
||
<h3>支給項目</h3>
|
||
<div class="detail-row"><span>基本賞与額:</span><span>¥${Number(
|
||
bonus.base_bonus || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>業績賞与:</span><span>¥${Number(
|
||
bonus.performance_bonus || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他賞与:</span><span>¥${Number(
|
||
bonus.other_bonus || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
|
||
bonus.total_bonus || 0
|
||
).toLocaleString()}</span></div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>控除項目</h3>
|
||
<div class="detail-row"><span>健康保険:</span><span>¥${Number(
|
||
bonus.health_insurance || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>介護保険:</span><span>¥${Number(
|
||
bonus.care_insurance || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(
|
||
bonus.pension_insurance || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(
|
||
bonus.employment_insurance || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${Number(
|
||
Number(bonus.health_insurance || 0) +
|
||
Number(bonus.care_insurance || 0) +
|
||
Number(bonus.pension_insurance || 0) +
|
||
Number(bonus.employment_insurance || 0)
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>所得税:</span><span>¥${Number(
|
||
bonus.income_tax || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||
bonus.resident_tax || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
|
||
bonus.other_deduction || 0
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
|
||
bonus.total_deduction || 0
|
||
).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(
|
||
bonus.net_bonus || 0
|
||
).toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" onclick="recalculateBonus(${
|
||
bonus.bonus_id
|
||
})">再計算</button>
|
||
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'">閉じる</button>
|
||
`;
|
||
|
||
document.getElementById("bonusDetail").innerHTML = html;
|
||
document.getElementById("bonusDetail").style.display = "block";
|
||
document
|
||
.getElementById("bonusDetail")
|
||
.scrollIntoView({ behavior: "smooth" });
|
||
} catch (error) {
|
||
alert("賞与明細の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function deleteBonus(bonusId) {
|
||
if (
|
||
!confirm("この賞与データを削除しますか?\nこの操作は取り消せません。")
|
||
)
|
||
return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert("賞与データを削除しました");
|
||
document.getElementById("bonusDetail").style.display = "none";
|
||
loadBonus();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(
|
||
"削除に失敗しました: " + JSON.stringify(error.detail || error)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
alert("削除に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function recalculateBonus(bonusId) {
|
||
if (!confirm("賞与を再計算しますか?")) return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/bonus/${bonusId}/recalculate`,
|
||
{
|
||
method: "POST",
|
||
}
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("再計算が完了しました");
|
||
viewBonus(bonusId);
|
||
} else {
|
||
const error = await response.json();
|
||
alert("再計算に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("再計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|