This commit is contained in:
admin
2026-03-04 23:20:42 +09:00
parent 99d16d8219
commit 31a64d5b59
11 changed files with 674 additions and 201 deletions

View File

@@ -195,14 +195,22 @@
<div id="tab-salary" class="tab-content-calc active">
<div class="filters">
<label>
対象年:
対象年:
<input
type="month"
id="filterYearMonth"
style="width: 160px"
placeholder="YYYY-MM"
type="number"
id="filterYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<label>
従業員:
<select id="filterEmployee" style="min-width: 200px">
<option value="">全員</option>
</select>
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">
新規計算
@@ -373,6 +381,34 @@
</div>
</div>
<div class="detail-section">
<h3>計算オプション</h3>
<div style="display: flex; gap: 30px; padding: 8px 0">
<label
style="
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
"
>
<input type="checkbox" id="calcIncomeTax" checked />
所得税を計算する
</label>
<label
style="
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
"
>
<input type="checkbox" id="calcSocialInsurance" checked />
社会保険を計算する
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">計算実行</button>
<button
type="button"
@@ -566,11 +602,66 @@
<div class="form-row">
<div class="form-group">
<label>開始年月 *</label>
<input type="month" id="voucherStartYearMonth" required />
<div style="display: flex; gap: 6px; align-items: center">
<input
type="text"
id="voucherStartYear"
style="width: 90px"
placeholder="年"
maxlength="4"
pattern="[0-9]{4}"
inputmode="numeric"
required
/>
<span style="color: #666"></span>
<select id="voucherStartMonth" 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>
<span style="color: #666"></span>
</div>
</div>
<div class="form-group">
<label>終了年月</label>
<input type="month" id="voucherEndYearMonth" />
<div style="display: flex; gap: 6px; align-items: center">
<input
type="text"
id="voucherEndYear"
style="width: 90px"
placeholder="年"
maxlength="4"
pattern="[0-9]{4}"
inputmode="numeric"
/>
<span style="color: #666"></span>
<select id="voucherEndMonth">
<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>
<span style="color: #666"></span>
</div>
</div>
<div class="form-group">
<label style="color: #999; font-size: 12px">
@@ -799,24 +890,41 @@
const bonusSelect = document.getElementById("bonusEmployeeSelect");
if (bonusSelect) bonusSelect.innerHTML = optionsHtml;
// 検索用フィルタのドロップダウンも更新
const filterSelect = document.getElementById("filterEmployee");
if (filterSelect) {
const currentVal = filterSelect.value;
filterSelect.innerHTML =
'<option value="">全員</option>' +
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
)
.join("");
filterSelect.value = currentVal;
}
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 detailDiv = document.getElementById("payrollDetail");
if (detailDiv) {
detailDiv.style.display = "none";
detailDiv.innerHTML = "";
}
const year =
Number(document.getElementById("filterYear")?.value) || null;
const employeeId =
document.getElementById("filterEmployee")?.value || null;
const params = new URLSearchParams();
if (year) params.append("payroll_year", year);
if (month) params.append("payroll_month", month);
if (employeeId) params.append("employee_id", employeeId);
// employeeMap が空なら読み込む
if (!employeeMap || Object.keys(employeeMap).length === 0) {
await loadEmployees();
@@ -827,52 +935,66 @@
`${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}`;
if (!payrolls || payrolls.length === 0) {
document.getElementById("payrollList").innerHTML =
"<p>給与データがありません</p>";
return;
}
// 月別にグループ化
const byMonth = {};
payrolls.forEach((p) => {
const key = `${p.payroll_year}-${String(p.payroll_month).padStart(2, "0")}`;
if (!byMonth[key]) byMonth[key] = [];
byMonth[key].push(p);
});
// 月順にソート
const sortedMonths = Object.keys(byMonth).sort();
const html = sortedMonths
.map((monthKey) => {
const [y, m] = monthKey.split("-");
const items = byMonth[monthKey]
.map((p) => {
const emp = employeeMap[p.employee_id];
const empLabel = emp
? `${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>${empLabel}</strong>
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
<span style="margin-left:8px; font-size:12px; color:#666;">支給日: ${p.payment_date}</span>
</div>
<div style="margin-top:4px;">
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
<span style="color:#666; font-size:13px;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()}</span>
</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("");
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>
`;
<div style="margin-bottom:20px;">
<div style="background:#e8f0fe; border-left:4px solid #007bff; padding:8px 14px; font-weight:bold; font-size:15px; margin-bottom:6px;">
${Number(y)}${Number(m)}
</div>
${items}
</div>
`;
})
.join("");
document.getElementById("payrollList").innerHTML =
html || "<p>給与データがありません</p>";
document.getElementById("payrollList").innerHTML = html;
} catch (error) {
alert("給与一覧の取得に失敗しました");
console.error(error);
@@ -927,6 +1049,11 @@
// ensure payroll_year/month are numbers
data.payroll_year = valid.year;
data.payroll_month = valid.month;
// 計算オプションフラグ
data.calc_income_tax =
document.getElementById("calcIncomeTax")?.checked ?? true;
data.calc_social_insurance =
document.getElementById("calcSocialInsurance")?.checked ?? true;
try {
const response = await fetch(
@@ -942,6 +1069,7 @@
const result = await response.json();
alert("給与計算が完了しました");
closeCalculateForm();
await loadPayrolls();
viewPayroll(result.payroll_id);
} else {
const error = await response.json();
@@ -1188,6 +1316,20 @@
</div>
</div>
<div class="detail-section">
<h3>計算オプション</h3>
<div style="display:flex; gap:30px; padding:8px 0;">
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="editCalcIncomeTax" checked />
所得税を計算する
</label>
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
<input type="checkbox" id="editCalcSocialInsurance" checked />
社会保険を計算する
</label>
</div>
</div>
<button type="submit" class="btn btn-primary">保存して再計算</button>
<button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button>
</form>
@@ -1224,11 +1366,20 @@
);
if (response.ok) {
// チェックボックスの値をフォーム削除前に取得
const calcIncomeTax =
document.getElementById("editCalcIncomeTax")?.checked ?? true;
const calcSocialInsurance =
document.getElementById("editCalcSocialInsurance")?.checked ??
true;
alert("給与データを更新しました");
form.closest("div").remove();
// 再計算
await recalculatePayroll(payrollId);
await recalculatePayroll(payrollId, {
calc_income_tax: calcIncomeTax,
calc_social_insurance: calcSocialInsurance,
});
} else {
const error = await response.json();
alert("更新に失敗しました: " + error.detail);
@@ -1266,14 +1417,17 @@
}
}
async function recalculatePayroll(payrollId) {
async function recalculatePayroll(payrollId, calcOptions = null) {
if (!confirm("給与を再計算しますか?")) return;
try {
const body = calcOptions || {};
const response = await fetch(
`${API_BASE}/payroll/calculation/${payrollId}/recalculate`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
},
);
@@ -1320,11 +1474,12 @@
}
}
// 初期化: 月ピッカーにセットして一覧を読み込む
// 初期化: 年フィルタに現在年をセットして一覧を読み込む
const now = new Date();
const ym =
now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
document.getElementById("filterYearMonth").value = ym;
const filterYearEl = document.getElementById("filterYear");
if (filterYearEl) filterYearEl.value = now.getFullYear();
document.getElementById("filterBonusYearMonth").value = ym;
loadPayrolls();
@@ -1359,8 +1514,11 @@
now.getFullYear() +
"-" +
String(now.getMonth() + 1).padStart(2, "0");
document.getElementById("voucherStartYearMonth").value = ym;
document.getElementById("voucherEndYearMonth").value = "";
document.getElementById("voucherStartYear").value = now.getFullYear();
document.getElementById("voucherStartMonth").value =
now.getMonth() + 1;
document.getElementById("voucherEndYear").value = "";
document.getElementById("voucherEndMonth").value = "";
}
}
@@ -1732,8 +1890,13 @@
async function printVouchers() {
// 検証
const startYM = document.getElementById("voucherStartYearMonth").value;
if (!startYM) {
const startYear = Number(
document.getElementById("voucherStartYear").value,
);
const startMonth = Number(
document.getElementById("voucherStartMonth").value,
);
if (!startYear || !startMonth) {
alert("開始年月を選択してください");
return;
}
@@ -1745,11 +1908,10 @@
}
// 年月を解析
const [startYear, startMonth] = startYM.split("-").map(Number);
const endYM = document.getElementById("voucherEndYearMonth").value;
const endParts = endYM ? endYM.split("-").map(Number) : null;
const endYear = endParts ? endParts[0] : null;
const endMonth = endParts ? endParts[1] : null;
const endYearVal = document.getElementById("voucherEndYear").value;
const endMonthVal = document.getElementById("voucherEndMonth").value;
const endYear = endYearVal ? Number(endYearVal) : null;
const endMonth = endMonthVal ? Number(endMonthVal) : null;
const voucherType = document.querySelector(
"input[name='voucherType']:checked",
@@ -1900,6 +2062,7 @@
<div class="page-title">${salary.payroll_year}${String(salary.payroll_month).padStart(2, "0")}月 給与明細</div>
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${paymentDate}</div>
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
<div class="section-title">勤怠情報</div>
<table class="detail-table">
@@ -2013,6 +2176,7 @@
<div class="page-title">${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}月 賞与明細${bonus.bonus_type ? ` (${bonus.bonus_type})` : ""}</div>
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${bonusPaymentDate}</div>
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
<div class="section-title">支給項目</div>
<table class="detail-table">
@@ -2103,8 +2267,13 @@
async function exportVouchersCSV() {
// 検証
const startYM = document.getElementById("voucherStartYearMonth").value;
if (!startYM) {
const startYear = Number(
document.getElementById("voucherStartYear").value,
);
const startMonth = Number(
document.getElementById("voucherStartMonth").value,
);
if (!startYear || !startMonth) {
alert("開始年月を選択してください");
return;
}
@@ -2115,11 +2284,10 @@
return;
}
const [startYear, startMonth] = startYM.split("-").map(Number);
const endYM = document.getElementById("voucherEndYearMonth").value;
const endParts = endYM ? endYM.split("-").map(Number) : null;
const endYear = endParts ? endParts[0] : null;
const endMonth = endParts ? endParts[1] : null;
const endYearVal = document.getElementById("voucherEndYear").value;
const endMonthVal = document.getElementById("voucherEndMonth").value;
const endYear = endYearVal ? Number(endYearVal) : null;
const endMonth = endMonthVal ? Number(endMonthVal) : null;
const voucherType = document.querySelector(
"input[name='voucherType']:checked",
).value;