1299 lines
44 KiB
HTML
1299 lines
44 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>
|
||
.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>
|