Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
admin
2026-04-30 10:12:08 +09:00
parent 5026282136
commit 6669c7da77
11 changed files with 4060 additions and 295 deletions

View File

@@ -2,13 +2,16 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.container {
max-width: 900px;
max-width: 1440px;
margin: 0 auto;
padding: 0 20px;
}
@@ -122,6 +125,15 @@
font-size: 1.2em;
color: #007bff;
}
.calc-note {
padding: 3px 10px 6px 14px;
font-size: 11px;
color: #555;
background: #f0f4f8;
border-left: 3px solid #b0c4de;
margin: -1px 0 4px 0;
line-height: 1.8;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
@@ -156,6 +168,40 @@
.tab-content-calc.active {
display: block;
}
/* 左右分割レイアウト */
.split-layout {
display: flex;
gap: 0;
align-items: flex-start;
}
.split-left {
flex: 0 0 420px;
min-width: 320px;
max-width: 420px;
max-height: calc(100vh - 160px);
overflow-y: auto;
position: sticky;
top: 20px;
}
.split-right {
display: none;
flex: 1;
min-width: 0;
border-left: 2px solid #e0e8f0;
padding-left: 24px;
max-height: calc(100vh - 160px);
overflow-y: auto;
position: sticky;
top: 20px;
}
.split-right .payroll-detail {
max-width: none;
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
margin-top: 0;
}
</style>
</head>
<body>
@@ -193,9 +239,11 @@
<!-- 給与計算タブ -->
<div id="tab-salary" class="tab-content-calc active">
<div class="filters">
<label>
対象年:
<div class="split-layout">
<div class="split-left">
<div class="filters" style="display:flex; flex-direction:column; gap:8px;">
<label style="display:flex; align-items:center; gap:8px;">
<span style="white-space:nowrap; min-width:60px;">対象年:</span>
<input
type="number"
id="filterYear"
@@ -205,19 +253,24 @@
max="2099"
/>
</label>
<label>
従業員:
<select id="filterEmployee" style="min-width: 200px">
<label style="display:flex; align-items:center; gap:8px;">
<span style="white-space:nowrap; min-width:60px;">従業員:</span>
<select id="filterEmployee" style="flex:1;">
<option value="">全員</option>
</select>
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">
新規計算
</button>
<div style="display:flex; gap:8px;">
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">新規計算</button>
</div>
</div>
<div id="payrollList" class="payroll-list"></div>
</div><!-- /split-left -->
<div class="split-right" id="splitRightSalary">
<div id="payrollDetail" class="payroll-detail" style="display:none"></div>
</div><!-- /split-right -->
</div><!-- /split-layout -->
<!-- 給与計算フォーム(モーダル風) -->
<div
@@ -420,16 +473,12 @@
</form>
</div>
<!-- 給与明細表示 -->
<div
id="payrollDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- 賞与計算タブ -->
<div id="tab-bonus" class="tab-content-calc">
<div class="split-layout">
<div class="split-left">
<div class="filters">
<label>
対象年:
@@ -449,6 +498,11 @@
</div>
<div id="bonusList" class="payroll-list"></div>
</div><!-- /split-left -->
<div class="split-right" id="splitRightBonus">
<div id="bonusDetail" class="payroll-detail" style="display:none"></div>
</div><!-- /split-right -->
</div><!-- /split-layout -->
<!-- 賞与計算フォーム -->
<div
@@ -584,12 +638,6 @@
</form>
</div>
<!-- 賞与明細表示 -->
<div
id="bonusDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- 账票出力タブ -->
@@ -966,18 +1014,16 @@
: `従業員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 style="flex:1; min-width:0; 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 style="font-size:12px; color:#666; margin-top:2px; white-space:nowrap;">支給日: ${p.payment_date}</div>
<div style="margin-top:4px; white-space:nowrap;"><strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong></div>
<div style="color:#666; font-size:13px; white-space:nowrap;">(総支給: ¥${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 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; flex-shrink:0;">
削除
</button>
</div>
@@ -1126,6 +1172,44 @@
}).length
: 0;
// 計算明細表示用の変数
const _health = Number(payroll.health_insurance || 0);
const _care = Number(payroll.care_insurance || 0);
const _pension = Number(payroll.pension_insurance || 0);
const _emp = Number(payroll.employment_insurance || 0);
const _child = Number(payroll.child_support || 0);
const _totalPay = Number(payroll.total_payment || 0);
// 標準報酬月額を厚生年金保険料(個人負担率 9.150%)から逆算
const _stdRemun = _pension > 0 ? Math.round(_pension / 0.0915) : 0;
const _stdFmt =
_stdRemun > 0 ? "¥" + _stdRemun.toLocaleString() : "-";
const _hRateFull =
_stdRemun > 0
? (((_health * 2) / _stdRemun) * 100).toFixed(2)
: "-";
const _hRateHalf =
_stdRemun > 0 ? ((_health / _stdRemun) * 100).toFixed(2) : "-";
const _cRateFull =
_stdRemun > 0 && _care > 0
? (((_care * 2) / _stdRemun) * 100).toFixed(2)
: null;
const _cRateHalf =
_stdRemun > 0 && _care > 0
? ((_care / _stdRemun) * 100).toFixed(2)
: null;
const _empRate =
_totalPay > 0 ? ((_emp / _totalPay) * 100).toFixed(3) : "-";
const _csRateFull =
_stdRemun > 0 && _child > 0
? (((_child * 2) / _stdRemun) * 100).toFixed(3)
: null;
const _csRateHalf =
_stdRemun > 0 && _child > 0
? ((_child / _stdRemun) * 100).toFixed(3)
: null;
const _socialSub = _health + _care + _pension + _emp + _child;
const _taxable = _totalPay - _socialSub;
const html = `
<h2>${payroll.payroll_year}${
payroll.payroll_month
@@ -1184,35 +1268,45 @@
<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>健康保険:</span><span>¥${_health.toLocaleString()}</span></div>
<div class="calc-note">
${_stdFmt}(標準報酬月額)× ${_hRateFull}%(保険料率・全体)÷ 2折半<br>
個人負担率:${_hRateHalf}% ※協会けんぽ東京
</div>
<div class="detail-row"><span>介護保険:</span><span>¥${_care.toLocaleString()}</span></div>
<div class="calc-note">
${
_cRateFull
? `${_stdFmt}(標準報酬月額)× ${_cRateFull}%(保険料率・全体)÷ 2折半<br>個人負担率:${_cRateHalf}% ※40歳以上65歳未満が対象`
: "介護保険非該当40歳未満または65歳以上"
}
</div>
<div class="detail-row"><span>厚生年金:</span><span>¥${_pension.toLocaleString()}</span></div>
<div class="calc-note">
${_stdFmt}(標準報酬月額)× 18.300%(保険料率・全体)÷ 2折半<br>
個人負担率9.150%(固定)
</div>
<div class="detail-row"><span>雇用保険:</span><span>¥${_emp.toLocaleString()}</span></div>
<div class="calc-note">
総支給額 ¥${_totalPay.toLocaleString()} × ${_empRate}%(雇用保険料率・従業員負担分)<br>
※標準報酬でなく実際の総支給額に料率を乗じる
</div>
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_child.toLocaleString()}</span></div>
<div class="calc-note">
${
_csRateFull
? `${_stdFmt}(標準報酬月額)× ${_csRateFull}%(拠出金率・全体)÷ 2折半<br>個人負担率:${_csRateHalf}% ※令和8年5月支払分4月分より控除開始`
: "令和8年5月支払分4月分保険料より控除開始 → 当月は対象外"
}
</div>
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div>
<div class="detail-row">
<span>所得税 <small style="color:#666;">(甲欄${dependentCount}人)</small>:</span>
<span>¥${Number(
payroll.income_tax,
).toLocaleString()}</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 class="calc-note">
課税対象額 = 総支給額 ¥${_totalPay.toLocaleString()} 社会保険料計 ¥${_socialSub.toLocaleString()} = ¥${_taxable.toLocaleString()}<br>
→ 源泉徴収税額表(甲欄・扶養${dependentCount})を参照して計算
</div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
payroll.resident_tax,
@@ -1242,14 +1336,13 @@
`
: ""
}
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'">閉じる</button>
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'; var sr=document.getElementById('splitRightSalary'); if(sr) sr.style.display='none';">閉じる</button>
`;
document.getElementById("payrollDetail").innerHTML = html;
const splitRightSalary = document.getElementById("splitRightSalary");
if (splitRightSalary) splitRightSalary.style.display = "block";
document.getElementById("payrollDetail").style.display = "block";
document
.getElementById("payrollDetail")
.scrollIntoView({ behavior: "smooth" });
} catch (error) {
alert("給与明細の取得に失敗しました");
console.error(error);
@@ -1435,6 +1528,7 @@
if (response.ok) {
alert("再計算が完了しました");
await loadPayrolls();
viewPayroll(payrollId);
} else {
const error = await response.json();
@@ -1552,34 +1646,18 @@
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 style="flex: 1; min-width: 0; 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>
<strong>${b.bonus_year}${b.bonus_month}月</strong>
<span class="status-badge status-calculated">${b.bonus_type}</span>
</div>
<div>
<div style="font-size: 12px; color: #666; margin-top: 2px; white-space: nowrap;">
${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 style="white-space: nowrap;"><strong>差引支給額: ¥${Number(b.net_bonus).toLocaleString()}</strong></div>
<div style="color: #666; font-size: 13px; white-space: nowrap;">(総支給: ¥${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 class="btn btn-danger btn-small" onclick="deleteBonus(${b.bonus_id}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; flex-shrink: 0; background-color: #dc3545; color: #fff; border: none;">
削除
</button>
</div>
@@ -1643,6 +1721,7 @@
const result = await response.json();
alert("賞与計算が完了しました");
closeBonusForm();
await loadBonus();
viewBonus(result.bonus_id);
} else {
const error = await response.json();
@@ -1669,6 +1748,25 @@
);
const employee = await empResponse.json();
// 計算明細表示用の変数(賞与は総支給額に料率を乗じる)
const _bHealth = Number(bonus.health_insurance || 0);
const _bCare = Number(bonus.care_insurance || 0);
const _bPension = Number(bonus.pension_insurance || 0);
const _bEmp = Number(bonus.employment_insurance || 0);
const _bChild = Number(bonus.child_support || 0);
const _bTotal = Number(bonus.total_bonus || 0);
const _bHRateFull = _bTotal > 0 && _bHealth > 0 ? (((_bHealth * 2) / _bTotal) * 100).toFixed(3) : "-";
const _bHRateHalf = _bTotal > 0 && _bHealth > 0 ? ((_bHealth / _bTotal) * 100).toFixed(3) : "-";
const _bCRateFull = _bTotal > 0 && _bCare > 0 ? (((_bCare * 2) / _bTotal) * 100).toFixed(3) : null;
const _bCRateHalf = _bTotal > 0 && _bCare > 0 ? ((_bCare / _bTotal) * 100).toFixed(3) : null;
const _bPRateFull = _bTotal > 0 && _bPension > 0 ? (((_bPension * 2) / _bTotal) * 100).toFixed(3) : "-";
const _bPRateHalf = _bTotal > 0 && _bPension > 0 ? ((_bPension / _bTotal) * 100).toFixed(3) : "-";
const _bEmpRate = _bTotal > 0 && _bEmp > 0 ? ((_bEmp / _bTotal) * 100).toFixed(3) : "-";
const _bCSRateFull = _bTotal > 0 && _bChild > 0 ? (((_bChild * 2) / _bTotal) * 100).toFixed(4) : null;
const _bCSRateHalf = _bTotal > 0 && _bChild > 0 ? ((_bChild / _bTotal) * 100).toFixed(4) : null;
const _bSocialSub = _bHealth + _bCare + _bPension + _bEmp + _bChild;
const _bTaxable = _bTotal - _bSocialSub;
const html = `
<h2>${bonus.bonus_year}${
bonus.bonus_month
@@ -1697,27 +1795,47 @@
<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>¥${_bHealth.toLocaleString()}</span></div>
<div class="calc-note">
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bHRateFull}%(保険料率・全体)÷ 2折半<br>
個人負担率:${_bHRateHalf}% ※協会けんぽ東京
</div>
<div class="detail-row"><span>介護保険:</span><span>¥${_bCare.toLocaleString()}</span></div>
<div class="calc-note">
${
_bCRateFull
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCRateFull}%(保険料率・全体)÷ 2折半<br>個人負担率:${_bCRateHalf}% ※40歳以上65歳未満が対象`
: "介護保険非該当40歳未満または65歳以上"
}
</div>
<div class="detail-row"><span>厚生年金:</span><span>¥${_bPension.toLocaleString()}</span></div>
<div class="calc-note">
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bPRateFull}%(保険料率・全体)÷ 2折半<br>
個人負担率:${_bPRateHalf}%固定 ※上限標準賞与額150万円/月
</div>
<div class="detail-row"><span>雇用保険:</span><span>¥${_bEmp.toLocaleString()}</span></div>
<div class="calc-note">
${_bEmp > 0
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
: "賞与に対する雇用保険は別途計算方式によるまたは0円"
}
</div>
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_bChild.toLocaleString()}</span></div>
<div class="calc-note">
${
_bCSRateFull
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCSRateFull}%(拠出金率・全体)÷ 2折半<br>個人負担率:${_bCSRateHalf}% ※令和8年5月支払分4月分より控除開始`
: "令和8年5月支払分4月分保険料より控除開始 → 当賞与は対象外"
}
</div>
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(
bonus.income_tax || 0,
).toLocaleString()}</span></div>
<div class="calc-note">
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}<br>
→ 賞与に対する源泉徴収税額の算出率の表を参照して計算
</div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
bonus.resident_tax || 0,
).toLocaleString()}</span></div>
@@ -1741,14 +1859,13 @@
<button class="btn btn-primary" onclick="recalculateBonus(${
bonus.bonus_id
})">再計算</button>
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'">閉じる</button>
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'; var sr=document.getElementById('splitRightBonus'); if(sr) sr.style.display='none';">閉じる</button>
`;
document.getElementById("bonusDetail").innerHTML = html;
const splitRightBonus = document.getElementById("splitRightBonus");
if (splitRightBonus) splitRightBonus.style.display = "block";
document.getElementById("bonusDetail").style.display = "block";
document
.getElementById("bonusDetail")
.scrollIntoView({ behavior: "smooth" });
} catch (error) {
alert("賞与明細の取得に失敗しました");
console.error(error);
@@ -1795,6 +1912,7 @@
if (response.ok) {
alert("再計算が完了しました");
await loadBonus();
viewBonus(bonusId);
} else {
const error = await response.json();

View File

@@ -215,6 +215,9 @@
<li>給与計算と賞与計算で異なる料率を設定できます</li>
<li>事業所所在地(都道府県)ごとに設定できます</li>
<li>介護保険の適用年齢は設定可能です(デフォルト: 40歳以上</li>
<li><strong>年度適用期間:</strong> 「年度」に設定した値は <strong>その年の4月〜翌年3月</strong> に適用されます。<br>
2026年度設定 → 2026年4月〜2027年3月の計算に参照されます。<br>
2026年1月〜3月分は 2025年度rate_year=2025の料率が参照されます。</li>
</ul>
</div>
@@ -802,6 +805,26 @@
pensionRate: metadata.pension_rate,
};
// メタデータに料率がない場合、データから逆算する
if (rates.healthNoCareRate == null) {
const refRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.health_insurance_no_care) > 0);
if (refRow) {
const ma = Number(refRow.monthly_amount);
rates.healthNoCareRate = Number(refRow.health_insurance_no_care) / ma * 100;
rates.healthWithCareRate = Number(refRow.health_insurance_with_care) / ma * 100;
if (Number(refRow.child_support ?? 0) > 0) {
rates.childSupportRate = Number(refRow.child_support) / ma * 100;
}
}
// 厚生年金は低等級(1-3)で0になるため、pension_insurance > 0 の行を別途検索
if (rates.pensionRate == null) {
const pensionRefRow = data.find(r => Number(r.monthly_amount) > 0 && Number(r.pension_insurance ?? 0) > 0);
if (pensionRefRow) {
rates.pensionRate = Number(pensionRefRow.pension_insurance) / Number(pensionRefRow.monthly_amount) * 100;
}
}
}
// メタデータはヘッダーに組み込むため、ここでは空にする
let metadataHtml = "";