Files
njts-accounting-core/frontend/payroll-settings.html
2026-01-20 11:15:49 +09:00

1299 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
flex-wrap: wrap;
}
.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;
}
.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;
box-sizing: border-box;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.form-row-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 15px;
}
.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;
}
.btn-small {
padding: 5px 10px;
font-size: 12px;
}
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;
}
.filter-section {
background: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.modal-form {
display: none;
margin-top: 20px;
padding: 20px;
border: 2px solid #007bff;
border-radius: 5px;
background: white;
}
.info-box {
background: #e7f3ff;
padding: 15px;
border-left: 4px solid #007bff;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 設定</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showTab('insurance')">
保険料率
</button>
<button class="nav-tab" onclick="showTab('child-support')">
子ども・子育て拠出金
</button>
<button class="nav-tab" onclick="showTab('limits')">上限設定</button>
<button class="nav-tab" onclick="showTab('tax')">所得税率表</button>
</div>
<!-- 保険料率タブ -->
<div id="tab-insurance" class="tab-content active">
<h2>社会保険料率設定</h2>
<div class="info-box">
<strong>設定のポイント:</strong>
<ul style="margin: 10px 0 0 20px">
<li>年度ごとに設定します(複数の料率を入力可能)</li>
<li>給与計算と賞与計算で異なる料率を設定できます</li>
<li>事業所所在地(都道府県)ごとに設定できます</li>
<li>介護保険の適用年齢は設定可能です(デフォルト: 40歳以上</li>
</ul>
</div>
<!-- フィルター -->
<div class="filter-section">
<div class="form-row-3">
<div class="form-group" style="margin-bottom: 0">
<label>年度</label>
<input
type="number"
id="filterInsuranceYear"
onchange="loadInsuranceRates()"
placeholder="全て"
min="2020"
max="2050"
style="width: 100%"
/>
<small style="color: #666">※空欄で全て表示</small>
</div>
<div class="form-group" style="margin-bottom: 0">
<label>計算対象</label>
<select
id="filterCalculationTarget"
onchange="loadInsuranceRates()"
>
<option value="">全て</option>
<option value="給与">給与</option>
<option value="賞与">賞与</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0">
<label>都道府県</label>
<select id="filterPrefecture" onchange="loadInsuranceRates()">
<option value="">全て</option>
<option value="東京都">東京都</option>
<option value="神奈川県">神奈川県</option>
<option value="千葉県">千葉県</option>
<option value="埼玉県">埼玉県</option>
</select>
</div>
</div>
</div>
<button class="btn btn-primary" onclick="showInsuranceForm()">
新規追加
</button>
<div id="insuranceList"></div>
<div id="insuranceForm" class="modal-form">
<h3 id="insuranceFormTitle">保険料率の登録</h3>
<form id="insuranceRateForm" onsubmit="saveInsuranceRate(event)">
<input type="hidden" name="rate_id" id="editRateId" />
<input type="hidden" name="edit_mode" id="editMode" value="false" />
<div class="form-row">
<div class="form-group">
<label>年度 *</label>
<input
type="number"
name="rate_year"
min="2020"
max="2050"
required
placeholder="例: 2026"
/>
</div>
<div class="form-group">
<label>計算対象 *</label>
<select name="calculation_target" required>
<option value="">選択してください</option>
<option value="給与">給与</option>
<option value="賞与">賞与</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>保険種類 *</label>
<select name="rate_type" required>
<option value="">選択してください</option>
<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="prefecture" required>
<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="number"
name="employee_rate"
step="0.001"
min="0"
max="100"
required
placeholder="例: 5.0 (5%)"
/>
</div>
<div class="form-group">
<label>会社負担率 (%) *</label>
<input
type="number"
name="employer_rate"
step="0.001"
min="0"
max="100"
required
placeholder="例: 5.0 (5%)"
/>
</div>
</div>
<div class="form-group">
<label>介護保険適用年齢下限</label>
<input
type="number"
name="care_insurance_age_threshold"
min="0"
max="100"
placeholder="例: 40 (40歳以上が対象。介護保険以外は空欄)"
/>
<small style="color: #666"
>※介護保険の場合のみ入力してください。この年齢以上の従業員に介護保険料が適用されます。</small
>
</div>
<div class="form-group">
<label>備考</label>
<textarea
name="notes"
rows="2"
placeholder="例: 健康保険料率 10.00% (労使折半)"
></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="hideInsuranceForm()"
>
キャンセル
</button>
</form>
</div>
</div>
<!-- 子ども・子育て拠出金タブ -->
<div id="tab-child-support" class="tab-content">
<h2>子ども・子育て拠出金率設定</h2>
<div class="info-box">
<strong>子ども・子育て拠出金について:</strong>
<ul style="margin: 10px 0 0 20px">
<li>事業主のみが負担する拠出金です(従業員負担なし)</li>
<li>標準報酬月額の上限額を設定できます</li>
<li>年度ごとに料率を設定します</li>
</ul>
</div>
<button class="btn btn-primary" onclick="showChildSupportForm()">
新規追加
</button>
<div id="childSupportList"></div>
<div id="childSupportForm" class="modal-form">
<h3 id="childSupportFormTitle">子ども・子育て拠出金率の登録</h3>
<form
id="childSupportRateForm"
onsubmit="saveChildSupportRate(event)"
>
<input
type="hidden"
name="contribution_id"
id="editContributionId"
/>
<input
type="hidden"
name="edit_mode"
id="editModeChild"
value="false"
/>
<div class="form-row">
<div class="form-group">
<label>年度 *</label>
<input
type="number"
name="rate_year"
min="2020"
max="2050"
required
placeholder="例: 2026"
/>
</div>
<div class="form-group">
<label>拠出金率 (%) *</label>
<input
type="number"
name="contribution_rate"
step="0.001"
min="0"
max="100"
required
placeholder="例: 0.36 (0.36%)"
/>
</div>
</div>
<div class="form-group">
<label>標準報酬月額上限 (円) *</label>
<input
type="number"
name="income_threshold"
min="0"
required
placeholder="例: 1500000 (150万円)"
/>
</div>
<div class="form-group">
<label>備考</label>
<textarea
name="notes"
rows="2"
placeholder="例: 子ども・子育て拠出金率 0.36% (事業主のみ負担)"
></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="hideChildSupportForm()"
>
キャンセル
</button>
</form>
</div>
</div>
<!-- 上限設定タブ -->
<div id="tab-limits" class="tab-content">
<h2>社会保険料上限設定</h2>
<div class="info-box">
<strong>上限設定について:</strong>
<ul style="margin: 10px 0 0 20px">
<li>健康保険と厚生年金の標準報酬月額上限を設定します</li>
<li>通勤手当の非課税限度額を設定します</li>
<li>年度ごとに変更される場合があります</li>
</ul>
</div>
<button class="btn btn-primary" onclick="showLimitForm()">
新規追加
</button>
<div id="limitsList"></div>
<div id="limitForm" class="modal-form">
<h3 id="limitFormTitle">上限設定の登録</h3>
<form id="limitSettingForm" onsubmit="saveLimitSetting(event)">
<input type="hidden" name="limit_id" id="editLimitId" />
<input
type="hidden"
name="edit_mode"
id="editModeLimit"
value="false"
/>
<div class="form-group">
<label>設定種別 *</label>
<select name="setting_type" required>
<option value="">選択してください</option>
<option value="健康保険標準報酬月額上限">
健康保険標準報酬月額上限
</option>
<option value="厚生年金標準報酬月額上限">
厚生年金標準報酬月額上限
</option>
<option value="健康保険標準賞与額上限">
健康保険標準賞与額上限(年間累計)
</option>
<option value="厚生年金標準賞与額上限">
厚生年金標準賞与額上限(月額)
</option>
<option value="通勤手当非課税限度額">
通勤手当非課税限度額
</option>
</select>
</div>
<div class="form-group">
<label>上限額 (円) *</label>
<input
type="number"
name="limit_amount"
min="0"
required
placeholder="例: 1390000"
/>
</div>
<div class="form-row">
<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" />
</div>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="hideLimitForm()"
>
キャンセル
</button>
</form>
</div>
</div>
<!-- 所得税率表タブ -->
<div id="tab-tax" class="tab-content">
<h2>所得税率表</h2>
<div class="form-group" style="max-width: 300px">
<label>扶養人数でフィルター</label>
<select id="filterDependents" onchange="loadIncomeTax()">
<option value="">全て</option>
<option value="0">0人</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>
</select>
</div>
<button
class="btn btn-primary"
onclick="document.getElementById('csvFile').click()"
>
Excel/CSVインポート
</button>
<input
type="file"
id="csvFile"
accept=".csv,.xlsx,.xls"
style="display: none"
onchange="importTaxTable(event)"
/>
<div id="taxList"></div>
</div>
</div>
<script>
const API_BASE = "http://localhost:18080";
// =====================================================
// タブ切り替え
// =====================================================
function showTab(tabName) {
document
.querySelectorAll(".nav-tab")
.forEach((tab) => tab.classList.remove("active"));
document
.querySelectorAll(".tab-content")
.forEach((content) => content.classList.remove("active"));
const tabs = {
insurance: 0,
"child-support": 1,
limits: 2,
tax: 3,
};
document
.querySelectorAll(".nav-tab")
[tabs[tabName]].classList.add("active");
document.getElementById(`tab-${tabName}`).classList.add("active");
// データをロード
if (tabName === "insurance") {
loadInsuranceRates();
} else if (tabName === "child-support") {
loadChildSupportRates();
} else if (tabName === "limits") {
loadLimitSettings();
} else if (tabName === "tax") {
loadIncomeTax();
}
}
// =====================================================
// 保険料率関連
// =====================================================
function showInsuranceForm() {
document.getElementById("insuranceForm").style.display = "block";
}
function hideInsuranceForm() {
document.getElementById("insuranceForm").style.display = "none";
document.querySelector("#insuranceForm form").reset();
}
async function loadInsuranceRates() {
try {
// フィルター値を取得
const year =
document.getElementById("filterInsuranceYear")?.value || "";
const target =
document.getElementById("filterCalculationTarget")?.value || "";
const prefecture =
document.getElementById("filterPrefecture")?.value || "";
// クエリパラメータを構築
const params = new URLSearchParams();
if (year) params.append("rate_year", year);
if (target) params.append("calculation_target", target);
if (prefecture) params.append("prefecture", prefecture);
const queryString = params.toString();
const url = `${API_BASE}/payroll/settings/insurance-rates${
queryString ? "?" + queryString : ""
}`;
const response = await fetch(url);
const rates = await response.json();
if (rates.length === 0) {
document.getElementById("insuranceList").innerHTML =
"<p>データがありません。</p>";
return;
}
const html = `
<table>
<thead>
<tr>
<th>年度</th>
<th>計算対象</th>
<th>保険種類</th>
<th>都道府県</th>
<th>従業員負担率</th>
<th>会社負担率</th>
<th>合計税率</th>
<th>介護保険年齢</th>
<th>備考</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${rates
.map((rate) => {
const employeeRate = parseFloat(rate.employee_rate);
const employerRate = parseFloat(rate.employer_rate);
const totalRate = employeeRate + employerRate;
return `
<tr>
<td>${rate.rate_year}</td>
<td>${rate.calculation_target}</td>
<td>${rate.rate_type}</td>
<td>${rate.prefecture}</td>
<td>${(employeeRate * 100).toFixed(3)}%</td>
<td>${(employerRate * 100).toFixed(3)}%</td>
<td><strong>${(totalRate * 100).toFixed(3)}%</strong></td>
<td>${
rate.care_insurance_age_threshold
? rate.care_insurance_age_threshold + "歳以上"
: "-"
}</td>
<td>${rate.notes || "-"}</td>
<td>
<button class="btn btn-primary btn-small" onclick="editInsuranceRate(${
rate.rate_id
})">編集</button>
<button class="btn btn-danger btn-small" onclick="deleteInsuranceRate(${
rate.rate_id
})">削除</button>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
`;
document.getElementById("insuranceList").innerHTML = html;
} catch (error) {
alert("保険料率の取得に失敗しました");
console.error(error);
}
}
async function editInsuranceRate(rateId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates`
);
const rates = await response.json();
const rate = rates.find((r) => r.rate_id === rateId);
if (!rate) {
alert("データが見つかりません");
return;
}
// フォームを編集モードに切り替え
document.getElementById("insuranceFormTitle").innerText =
"保険料率の編集";
document.getElementById("editMode").value = "true";
document.getElementById("editRateId").value = rateId;
// フォームにデータを入力
const form = document.getElementById("insuranceRateForm");
form.querySelector('[name="rate_year"]').value = rate.rate_year;
form.querySelector('[name="calculation_target"]').value =
rate.calculation_target;
form.querySelector('[name="rate_type"]').value = rate.rate_type;
form.querySelector('[name="prefecture"]').value = rate.prefecture;
form.querySelector('[name="employee_rate"]').value = (
rate.employee_rate * 100
).toFixed(3);
form.querySelector('[name="employer_rate"]').value = (
rate.employer_rate * 100
).toFixed(3);
form.querySelector('[name="care_insurance_age_threshold"]').value =
rate.care_insurance_age_threshold || "";
form.querySelector('[name="notes"]').value = rate.notes || "";
// フォームを表示
document.getElementById("insuranceForm").style.display = "block";
} catch (error) {
alert("データの取得に失敗しました");
console.error(error);
}
}
async function saveInsuranceRate(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const isEditMode = data.edit_mode === "true";
const rateId = data.rate_id;
console.log("=== 保存前のデータ ===");
console.log("編集モード:", isEditMode);
console.log("Rate ID:", rateId);
console.log("従業員負担率 (入力値):", data.employee_rate);
console.log("会社負担率 (入力値):", data.employer_rate);
// 不要なフィールドを削除
delete data.edit_mode;
delete data.rate_id;
// パーセントを小数に変換
data.employee_rate = parseFloat(data.employee_rate) / 100;
data.employer_rate = parseFloat(data.employer_rate) / 100;
console.log("従業員負担率 (変換後):", data.employee_rate);
console.log("会社負担率 (変換後):", data.employer_rate);
// 年度と計算対象を数値/文字列に変換
data.rate_year = parseInt(data.rate_year);
// 介護保険年齢を数値に変換空の場合はnull
if (data.care_insurance_age_threshold) {
data.care_insurance_age_threshold = parseInt(
data.care_insurance_age_threshold
);
} else {
data.care_insurance_age_threshold = null;
}
// 空の値を除外
if (!data.notes) delete data.notes;
console.log(
"送信するURL:",
isEditMode
? `${API_BASE}/payroll/settings/insurance-rates/${rateId}`
: `${API_BASE}/payroll/settings/insurance-rates`
);
console.log("送信するデータ:", JSON.stringify(data, null, 2));
try {
const url = isEditMode
? `${API_BASE}/payroll/settings/insurance-rates/${rateId}`
: `${API_BASE}/payroll/settings/insurance-rates`;
const method = isEditMode ? "PUT" : "POST";
console.log("HTTPメソッド:", method);
const response = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
console.log("レスポンスステータス:", response.status);
if (response.ok) {
const result = await response.json();
console.log("成功レスポンス:", result);
alert(
isEditMode ? "保険料率を更新しました" : "保険料率を登録しました"
);
hideInsuranceForm();
loadInsuranceRates();
} else {
const error = await response.json();
console.error("エラーレスポンス:", error);
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
);
}
} catch (error) {
console.error("例外が発生:", error);
alert((isEditMode ? "更新" : "登録") + "に失敗しました");
console.error(error);
}
}
async function deleteInsuranceRate(rateId) {
if (!confirm("この保険料率を削除してもよろしいですか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates/${rateId}`,
{ method: "DELETE" }
);
if (response.ok) {
alert("削除しました");
loadInsuranceRates();
} else {
alert("削除に失敗しました");
}
} catch (error) {
alert("削除に失敗しました");
console.error(error);
}
}
// =====================================================
// 子ども・子育て拠出金関連
// =====================================================
function showChildSupportForm() {
document.getElementById("childSupportForm").style.display = "block";
}
function hideChildSupportForm() {
document.getElementById("childSupportForm").style.display = "none";
document.getElementById("childSupportFormTitle").innerText =
"子ども・子育て拠出金率の登録";
document.getElementById("editModeChild").value = "false";
document.getElementById("editContributionId").value = "";
document.getElementById("childSupportRateForm").reset();
}
async function loadChildSupportRates() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates`
);
const rates = await response.json();
if (rates.length === 0) {
document.getElementById("childSupportList").innerHTML =
"<p>データがありません。</p>";
return;
}
const html = `
<table>
<thead>
<tr>
<th>年度</th>
<th>標準報酬月額上限</th>
<th>拠出金率</th>
<th>備考</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${rates
.map(
(rate) => `
<tr>
<td>${rate.rate_year}</td>
<td>¥${Number(
rate.income_threshold
).toLocaleString()}</td>
<td>${(parseFloat(rate.contribution_rate) * 100).toFixed(
3
)}%</td>
<td>${rate.notes || "-"}</td>
<td>
<button class="btn btn-primary btn-small" onclick="editChildSupportRate(${
rate.contribution_id
})">編集</button>
<button class="btn btn-danger btn-small" onclick="deleteChildSupportRate(${
rate.contribution_id
})">削除</button>
</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("childSupportList").innerHTML = html;
} catch (error) {
alert("子ども・子育て拠出金率の取得に失敗しました");
console.error(error);
}
}
async function editChildSupportRate(contributionId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates`
);
const rates = await response.json();
const rate = rates.find((r) => r.contribution_id === contributionId);
if (!rate) {
alert("データが見つかりません");
return;
}
// フォームを編集モードに切り替え
document.getElementById("childSupportFormTitle").innerText =
"子ども・子育て拠出金率の編集";
document.getElementById("editModeChild").value = "true";
document.getElementById("editContributionId").value = contributionId;
// フォームにデータを入力
const form = document.getElementById("childSupportRateForm");
form.querySelector('[name="rate_year"]').value = rate.rate_year;
form.querySelector('[name="income_threshold"]').value =
rate.income_threshold;
form.querySelector('[name="contribution_rate"]').value = (
rate.contribution_rate * 100
).toFixed(3);
form.querySelector('[name="notes"]').value = rate.notes || "";
// フォームを表示
document.getElementById("childSupportForm").style.display = "block";
} catch (error) {
alert("データの取得に失敗しました");
console.error(error);
}
}
async function saveChildSupportRate(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const isEditMode = data.edit_mode === "true";
const contributionId = data.contribution_id;
// 不要なフィールドを削除
delete data.edit_mode;
delete data.contribution_id;
// データ型変換
data.rate_year = parseInt(data.rate_year);
data.income_threshold = parseFloat(data.income_threshold);
data.contribution_rate = parseFloat(data.contribution_rate) / 100;
// 空の値を除外
if (!data.notes) delete data.notes;
try {
const url = isEditMode
? `${API_BASE}/payroll/settings/child-support-rates/${contributionId}`
: `${API_BASE}/payroll/settings/child-support-rates`;
const method = isEditMode ? "PUT" : "POST";
const response = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert(
isEditMode
? "子ども・子育て拠出金率を更新しました"
: "子ども・子育て拠出金率を登録しました"
);
hideChildSupportForm();
loadChildSupportRates();
} else {
const error = await response.json();
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
);
}
} catch (error) {
alert((isEditMode ? "更新" : "登録") + "に失敗しました");
console.error(error);
}
}
async function deleteChildSupportRate(contributionId) {
if (!confirm("この拠出金率を削除してもよろしいですか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates/${contributionId}`,
{ method: "DELETE" }
);
if (response.ok) {
alert("削除しました");
loadChildSupportRates();
} else {
alert("削除に失敗しました");
}
} catch (error) {
alert("削除に失敗しました");
console.error(error);
}
}
// =====================================================
// 上限設定関連
// =====================================================
function showLimitForm() {
document.getElementById("limitForm").style.display = "block";
}
function hideLimitForm() {
document.getElementById("limitForm").style.display = "none";
document.getElementById("limitFormTitle").innerText = "上限設定の登録";
document.getElementById("editModeLimit").value = "false";
document.getElementById("editLimitId").value = "";
document.getElementById("limitSettingForm").reset();
}
async function loadLimitSettings() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits`
);
const limits = await response.json();
if (limits.length === 0) {
document.getElementById("limitsList").innerHTML =
"<p>データがありません。</p>";
return;
}
const html = `
<table>
<thead>
<tr>
<th>設定種別</th>
<th>上限額</th>
<th>適用期間</th>
<th>備考</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${limits
.map(
(limit) => `
<tr>
<td>${limit.setting_type}</td>
<td>¥${Number(limit.limit_amount).toLocaleString()}</td>
<td>${limit.valid_from} ${
limit.valid_to || "現在"
}</td>
<td>${limit.notes || "-"}</td>
<td>
<button class="btn btn-primary btn-small" onclick="editLimitSetting(${
limit.limit_id
})">編集</button>
<button class="btn btn-danger btn-small" onclick="deleteLimitSetting(${
limit.limit_id
})">削除</button>
</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("limitsList").innerHTML = html;
} catch (error) {
alert("上限設定の取得に失敗しました");
console.error(error);
}
}
async function editLimitSetting(limitId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits`
);
const limits = await response.json();
const limit = limits.find((l) => l.limit_id === limitId);
if (!limit) {
alert("データが見つかりません");
return;
}
// フォームを編集モードに切り替え
document.getElementById("limitFormTitle").innerText =
"上限設定の編集";
document.getElementById("editModeLimit").value = "true";
document.getElementById("editLimitId").value = limitId;
// フォームにデータを入力
const form = document.getElementById("limitSettingForm");
form.querySelector('[name="setting_type"]').value =
limit.setting_type;
form.querySelector('[name="limit_amount"]').value =
limit.limit_amount;
form.querySelector('[name="valid_from"]').value = limit.valid_from;
form.querySelector('[name="valid_to"]').value = limit.valid_to || "";
form.querySelector('[name="notes"]').value = limit.notes || "";
// フォームを表示
document.getElementById("limitForm").style.display = "block";
} catch (error) {
alert("データの取得に失敗しました");
console.error(error);
}
}
async function saveLimitSetting(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
const isEditMode = data.edit_mode === "true";
const limitId = data.limit_id;
// 不要なフィールドを削除
delete data.edit_mode;
delete data.limit_id;
// データ型変換
data.limit_amount = parseFloat(data.limit_amount);
// 空の値を除外
if (!data.valid_to) delete data.valid_to;
if (!data.notes) delete data.notes;
try {
const url = isEditMode
? `${API_BASE}/payroll/settings/insurance-limits/${limitId}`
: `${API_BASE}/payroll/settings/insurance-limits`;
const method = isEditMode ? "PUT" : "POST";
const response = await fetch(url, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert(
isEditMode ? "上限設定を更新しました" : "上限設定を登録しました"
);
hideLimitForm();
loadLimitSettings();
} else {
const error = await response.json();
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
);
}
} catch (error) {
alert((isEditMode ? "更新" : "登録") + "に失敗しました");
console.error(error);
}
}
async function deleteLimitSetting(limitId) {
if (!confirm("この上限設定を削除してもよろしいですか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits/${limitId}`,
{ method: "DELETE" }
);
if (response.ok) {
alert("削除しました");
loadLimitSettings();
} else {
alert("削除に失敗しました");
}
} catch (error) {
alert("削除に失敗しました");
console.error(error);
}
}
// =====================================================
// 所得税率表関連
// =====================================================
// =====================================================
// 所得税率表関連
// =====================================================
async function loadIncomeTax() {
const dependentsFilter =
document.getElementById("filterDependents").value;
const params = dependentsFilter
? `?dependents_count=${dependentsFilter}`
: "";
try {
const response = await fetch(
`${API_BASE}/payroll/settings/income-tax${params}`
);
const taxes = await response.json();
if (taxes.length === 0) {
document.getElementById("taxList").innerHTML =
"<p>データがありません。</p>";
return;
}
const html = `
<table>
<thead>
<tr>
<th>扶養人数</th>
<th>月収From</th>
<th>月収To</th>
<th>税額</th>
<th>適用期間</th>
</tr>
</thead>
<tbody>
${taxes
.map(
(tax) => `
<tr>
<td>${tax.dependents_count}人</td>
<td>¥${Number(
tax.monthly_income_from
).toLocaleString()}</td>
<td>¥${Number(
tax.monthly_income_to
).toLocaleString()}</td>
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
<td>${tax.valid_from} ${tax.valid_to || "現在"}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("taxList").innerHTML = html;
} catch (error) {
alert("所得税率表の取得に失敗しました");
console.error(error);
}
}
async function importTaxTable(event) {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append("file", file);
const taxYear = prompt(
"税年度を入力してください(空欄の場合はファイルから自動抽出):",
""
);
try {
let url = `${API_BASE}/payroll/settings/income-tax/import`;
if (taxYear && taxYear.trim()) {
url += `?tax_year=${taxYear.trim()}`;
}
const response = await fetch(url, {
method: "POST",
body: formData,
});
if (response.ok) {
const result = await response.json();
alert(result.message + `\n年度: ${result.year}`);
loadIncomeTax();
} else {
const error = await response.json();
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
}
} catch (error) {
alert("インポートに失敗しました");
console.error(error);
}
event.target.value = "";
}
// =====================================================
// 初期化
// =====================================================
document.addEventListener("DOMContentLoaded", function () {
loadInsuranceRates();
});
</script>
</body>
</html>