This commit is contained in:
admin
2026-05-14 22:02:06 +09:00
parent 78022b3b15
commit b9f994b1e1
22 changed files with 1225 additions and 126 deletions

View File

@@ -2,7 +2,10 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<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>
@@ -241,36 +244,54 @@
<div id="tab-salary" class="tab-content-calc active">
<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"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<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>
<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
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"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<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>
<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 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
id="payrollDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- /split-right -->
</div>
<!-- /split-layout -->
<!-- 給与計算フォーム(モーダル風) -->
<div
@@ -472,37 +493,45 @@
</button>
</form>
</div>
</div>
<!-- 賞与計算タブ -->
<div id="tab-bonus" class="tab-content-calc">
<div class="split-layout">
<div class="split-left">
<div class="filters">
<label>
対象年:
<input
type="number"
id="filterBonusYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</label>
<button class="btn btn-primary" onclick="loadBonus()">検索</button>
<button class="btn btn-success" onclick="showBonusForm()">
新規計算
</button>
</div>
<div class="filters">
<label>
対象年:
<input
type="number"
id="filterBonusYear"
style="width: 90px"
value="2026"
min="2000"
max="2099"
/>
</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><!-- /split-left -->
<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
id="bonusDetail"
class="payroll-detail"
style="display: none"
></div>
</div>
<!-- /split-right -->
</div>
<!-- /split-layout -->
<!-- 賞与計算フォーム -->
<div
@@ -637,7 +666,6 @@
</button>
</form>
</div>
</div>
<!-- 账票出力タブ -->
@@ -1750,20 +1778,64 @@
// 計算明細表示用の変数(賞与は総支給額に料率を乗じる)
const _bHealth = Number(bonus.health_insurance || 0);
// 源泉税控除対象人数を計算(給与と同じロジック)
const bDependentCount = employee.dependents
? employee.dependents.filter((d) => {
if (d.valid_to && new Date(d.valid_to) < new Date()) return false;
if (!d.birth_date) return false;
const birthDate = new Date(d.birth_date);
const targetYear = bonus.bonus_year;
const yearEnd = new Date(targetYear, 11, 31);
let age = yearEnd.getFullYear() - birthDate.getFullYear();
const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())) {
age--;
}
return age >= 16;
}).length
: 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 _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;
@@ -1815,9 +1887,10 @@
</div>
<div class="detail-row"><span>雇用保険:</span><span>¥${_bEmp.toLocaleString()}</span></div>
<div class="calc-note">
${_bEmp > 0
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
: "賞与に対する雇用保険は別途計算方式によるまたは0円"
${
_bEmp > 0
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
: "賞与に対する雇用保険は別途計算方式によるまたは0円"
}
</div>
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_bChild.toLocaleString()}</span></div>
@@ -1829,12 +1902,13 @@
}
</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="detail-row">
<span>所得税 <small style="color:#666;">(甲欄${bDependentCount}人)</small>:</span>
<span>¥${Number(bonus.income_tax || 0).toLocaleString()}</span>
</div>
<div class="calc-note">
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}<br>
→ 賞与に対する源泉徴収税額の算出率の表を参照して計算
→ 賞与に対する源泉徴収税額の算出率の表(甲欄・扶養${bDependentCount}人)を参照して計算
</div>
<div class="detail-row"><span>住民税:</span><span>¥${Number(
bonus.resident_tax || 0,
@@ -1856,6 +1930,9 @@
</div>
</div>
<button class="btn btn-primary" onclick="editBonus(${
bonus.bonus_id
})">編集</button>
<button class="btn btn-primary" onclick="recalculateBonus(${
bonus.bonus_id
})">再計算</button>
@@ -1872,6 +1949,105 @@
}
}
async function editBonus(bonusId) {
try {
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`);
const bonus = await response.json();
const html = `
<div style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 600px; 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="editBonusForm" onsubmit="saveBonusEdit(event, ${bonusId})">
<div class="detail-section">
<h3>基本情報</h3>
<div class="form-row">
<div class="form-group">
<label>支給日</label>
<input type="date" name="payment_date" value="${bonus.payment_date}" required />
</div>
<div class="form-group">
<label>賞与種別</label>
<select name="bonus_type">
<option value="夏季賞与" ${bonus.bonus_type === '夏季賞与' ? 'selected' : ''}>夏季賞与</option>
<option value="冬季賞与" ${bonus.bonus_type === '冬季賞与' ? 'selected' : ''}>冬季賞与</option>
<option value="決算賞与" ${bonus.bonus_type === '決算賞与' ? 'selected' : ''}>決算賞与</option>
<option value="その他" ${bonus.bonus_type === 'その他' ? 'selected' : ''}>その他</option>
</select>
</div>
</div>
</div>
<div class="detail-section">
<h3>支給項目</h3>
<div class="form-row">
<div class="form-group">
<label>基本賞与額</label>
<input type="number" name="base_bonus" step="1" value="${bonus.base_bonus}" required />
</div>
<div class="form-group">
<label>業績賞与</label>
<input type="number" name="performance_bonus" step="1" value="${bonus.performance_bonus}" />
</div>
<div class="form-group">
<label>その他賞与</label>
<input type="number" name="other_bonus" step="1" value="${bonus.other_bonus}" />
</div>
</div>
</div>
<div class="detail-section">
<h3>控除項目(手動入力)</h3>
<div class="form-row">
<div class="form-group">
<label>その他控除</label>
<input type="number" name="other_deduction" step="1" value="${bonus.other_deduction}" />
</div>
</div>
</div>
<p style="font-size:12px; color:#666;">※保存後に再計算を行うと社会保険料・所得税が自動更新されます。</p>
<button type="submit" class="btn btn-primary">保存して再計算</button>
<button type="button" class="btn btn-secondary" onclick="this.closest('div').parentElement.remove()">キャンセル</button>
</form>
</div>
`;
const editDiv = document.createElement("div");
editDiv.innerHTML = html;
document.body.appendChild(editDiv);
} catch (error) {
alert("賞与データの取得に失敗しました");
console.error(error);
}
}
async function saveBonusEdit(event, bonusId) {
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/bonus/${bonusId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("賞与データを更新しました");
form.closest("div").parentElement.remove();
await recalculateBonus(bonusId, true);
} else {
const error = await response.json();
alert("更新に失敗しました: " + error.detail);
}
} catch (error) {
alert("更新に失敗しました");
console.error(error);
}
}
async function deleteBonus(bonusId) {
if (
!confirm("この賞与データを削除しますか?\nこの操作は取り消せません。")
@@ -1899,8 +2075,8 @@
}
}
async function recalculateBonus(bonusId) {
if (!confirm("賞与を再計算しますか?")) return;
async function recalculateBonus(bonusId, skipConfirm = false) {
if (!skipConfirm && !confirm("賞与を再計算しますか?")) return;
try {
const response = await fetch(
@@ -2270,10 +2446,14 @@
bonuses.forEach((bonus) => {
const bonusYearMonth = `${bonus.bonus_year}-${bonus.bonus_month}`;
const currentKey_new = `${emp.employee_id}-${bonusYearMonth}-bonus`;
const bonusPaymentDate = `${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}`;
// 新しいキーが異なる場合は新しいページを開く
if (currentKey !== currentKey_new) {
// DBに保存された実際の支給日を使用。なければ月末日を計算
const bonusPaymentDate = bonus.payment_date
? (() => {
const d = new Date(bonus.payment_date);
return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;
})()
: `${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}`;
if (currentKey && currentKey !== currentKey_new) {
if (pageOpen) {
printHtml += "</div>";
}