2031 lines
71 KiB
HTML
2031 lines
71 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;
|
||
}
|
||
.year-item {
|
||
padding: 15px;
|
||
margin: 10px 0;
|
||
background: #f9f9f9;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
transition: background 0.2s;
|
||
}
|
||
.year-item:hover {
|
||
background: #e8f4ff;
|
||
}
|
||
.year-item.expanded {
|
||
background: #e8f4ff;
|
||
border-color: #007bff;
|
||
}
|
||
.year-header {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
.year-data {
|
||
display: none;
|
||
margin-top: 15px;
|
||
}
|
||
.year-item.expanded .year-data {
|
||
display: block;
|
||
}
|
||
.filter-controls {
|
||
margin-bottom: 15px;
|
||
padding: 10px;
|
||
background: #fff;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
.back-link {
|
||
display: inline-block;
|
||
margin-bottom: 20px;
|
||
color: #007bff;
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
}
|
||
.back-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<a href="payroll.html" class="back-link">← 給与管理メニューに戻る</a>
|
||
|
||
<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>
|
||
<button class="nav-tab" onclick="showTab('standard-remuneration')">
|
||
標準報酬月額表
|
||
</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
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>適用終了日</label>
|
||
<input
|
||
type="date"
|
||
name="valid_to"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</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>所得税率表(年度1月~12月)</h2>
|
||
<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="taxYearsList" style="margin-top: 20px"></div>
|
||
<div id="taxDataContainer" style="margin-top: 20px"></div>
|
||
</div>
|
||
|
||
<!-- 標準報酬月額表タブ -->
|
||
<div id="tab-standard-remuneration" class="tab-content">
|
||
<h2>標準報酬月額表(年度3月~翌年2月)</h2>
|
||
<div class="info-box">
|
||
<strong>標準報酬月額表について:</strong>
|
||
<ul style="margin: 10px 0 0 20px">
|
||
<li>健康保険・厚生年金保険の保険料額表をインポートします</li>
|
||
<li>都道府県ごとの料率表に対応しています</li>
|
||
<li>年度をクリックして展開してください</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div style="margin: 20px 0">
|
||
<button
|
||
class="btn btn-primary"
|
||
onclick="document.getElementById('stdRemunerationFile').click()"
|
||
>
|
||
Excel/CSVインポート
|
||
</button>
|
||
<input
|
||
type="file"
|
||
id="stdRemunerationFile"
|
||
accept=".csv,.xlsx,.xls"
|
||
style="display: none"
|
||
onchange="importStdRemunerationTable(event)"
|
||
/>
|
||
</div>
|
||
|
||
<div id="stdRemunerationYearsList" style="margin-top: 20px"></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,
|
||
"standard-remuneration": 4,
|
||
};
|
||
|
||
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();
|
||
} else if (tabName === "standard-remuneration") {
|
||
loadStdRemunerationYears();
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 標準報酬月額表関連
|
||
// =====================================================
|
||
async function loadStdRemunerationYears() {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/standard-remuneration/years`
|
||
);
|
||
const years = await response.json();
|
||
|
||
if (!years || years.length === 0) {
|
||
document.getElementById("stdRemunerationYearsList").innerHTML =
|
||
"<p>データがありません。インポートしてください。</p>";
|
||
return;
|
||
}
|
||
|
||
const html = years
|
||
.sort((a, b) => b - a)
|
||
.map(
|
||
(year) => `
|
||
<div class="year-item" id="std-year-${year}">
|
||
<div class="year-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div style="cursor: pointer; flex: 1;" onclick="toggleStdRemunerationYear(${year})">
|
||
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deleteStdRemunerationYear(${year})" style="margin-left: 10px;">
|
||
削除
|
||
</button>
|
||
</div>
|
||
<div class="year-data" id="std-data-${year}">
|
||
<div id="std-table-${year}">読み込み中...</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
)
|
||
.join("");
|
||
|
||
document.getElementById("stdRemunerationYearsList").innerHTML = html;
|
||
} catch (error) {
|
||
console.error("年度リストの取得に失敗:", error);
|
||
document.getElementById("stdRemunerationYearsList").innerHTML =
|
||
'<p style="color:red;">年度リストの取得に失敗しました</p>';
|
||
}
|
||
}
|
||
|
||
async function toggleStdRemunerationYear(year) {
|
||
const yearItem = document.getElementById(`std-year-${year}`);
|
||
const wasExpanded = yearItem.classList.contains("expanded");
|
||
|
||
// 他の年度を閉じる
|
||
document
|
||
.querySelectorAll("#stdRemunerationYearsList .year-item")
|
||
.forEach((item) => {
|
||
item.classList.remove("expanded");
|
||
});
|
||
|
||
if (!wasExpanded) {
|
||
yearItem.classList.add("expanded");
|
||
currentExpandedStdYear = year;
|
||
await loadStdRemunerationYearData(year);
|
||
} else {
|
||
currentExpandedStdYear = null;
|
||
}
|
||
}
|
||
|
||
async function loadStdRemunerationYearData(year) {
|
||
const params = new URLSearchParams();
|
||
params.append("year", year);
|
||
const url = `${API_BASE}/payroll/settings/standard-remuneration?${params.toString()}`;
|
||
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
|
||
const data = await response.json();
|
||
|
||
if (!Array.isArray(data) || data.length === 0) {
|
||
document.getElementById(`std-table-${year}`).innerHTML =
|
||
"<p>該当するデータがありません</p>";
|
||
return;
|
||
}
|
||
|
||
// セッションストレージからメタデータを取得
|
||
const metadata = JSON.parse(
|
||
sessionStorage.getItem("stdRemunMetadata") || "{}"
|
||
);
|
||
console.log("取得したメタデータ:", metadata);
|
||
|
||
// 都道府県ごとにグループ化
|
||
const prefectures = {};
|
||
data.forEach((row) => {
|
||
if (!prefectures[row.prefecture]) {
|
||
prefectures[row.prefecture] = [];
|
||
}
|
||
prefectures[row.prefecture].push(row);
|
||
});
|
||
|
||
// 都道府県ごとにテーブルを作成
|
||
let tablesHtml = "";
|
||
for (const [prefecture, prefData] of Object.entries(prefectures)) {
|
||
tablesHtml += `<h3 style="margin-top: 20px; margin-bottom: 10px; color: #333;">${prefecture}</h3>`;
|
||
tablesHtml += renderStdRemunerationTable(prefData, metadata);
|
||
}
|
||
|
||
document.getElementById(`std-table-${year}`).innerHTML = tablesHtml;
|
||
} catch (error) {
|
||
console.error("データ読み込みエラー:", error);
|
||
document.getElementById(
|
||
`std-table-${year}`
|
||
).innerHTML = `<p style="color:red;">データの取得に失敗しました: ${error.message}</p>`;
|
||
}
|
||
}
|
||
|
||
function renderStdRemunerationTable(data, metadata = {}) {
|
||
if (!data || data.length === 0) {
|
||
return "<p>データがありません。</p>";
|
||
}
|
||
|
||
// A1セルの内容を表示(あれば)
|
||
let sourceHtml = "";
|
||
console.log("renderStdRemunerationTable - metadata内容:", metadata);
|
||
console.log(
|
||
"renderStdRemunerationTable - metadata.source:",
|
||
metadata.source
|
||
);
|
||
if (metadata.source) {
|
||
sourceHtml = `<p style="margin-bottom: 15px; padding: 10px; background: #f0f8ff; border-left: 3px solid #0066cc; color: #333;"><strong>対象期間:</strong> ${metadata.source}</p>`;
|
||
} else {
|
||
console.warn("⚠️ metadata.sourceが空です");
|
||
}
|
||
|
||
// メタデータから保険料率を取得
|
||
const rates = {
|
||
healthNoCareRate: metadata.health_no_care_rate,
|
||
healthWithCareRate: metadata.health_with_care_rate,
|
||
pensionRate: metadata.pension_rate,
|
||
};
|
||
|
||
// メタデータはヘッダーに組み込むため、ここでは空にする
|
||
let metadataHtml = "";
|
||
|
||
// 等級でソート(最初の数字を抽出して数値順にソート)
|
||
const sortedData = data.sort((a, b) => {
|
||
const gradeA = String(a.grade).trim();
|
||
const gradeB = String(b.grade).trim();
|
||
|
||
// 括弧や記号を除いて、最初の数字を抽出
|
||
const numA = parseInt(gradeA.match(/\d+/)?.[0] || 0);
|
||
const numB = parseInt(gradeB.match(/\d+/)?.[0] || 0);
|
||
|
||
// 数値で比較
|
||
if (numA !== numB) {
|
||
return numA - numB;
|
||
}
|
||
|
||
// 同じ数値の場合は元の文字列で比較
|
||
return gradeA.localeCompare(gradeB, "ja");
|
||
});
|
||
|
||
// ペンション値の段階的な埋め充(2段階)
|
||
// ステップ1: 0の値に対して、下方を探して最初の非ゼロ値を使用(向下查找)
|
||
const step1Data = [...sortedData];
|
||
for (let i = 0; i < step1Data.length; i++) {
|
||
const pensionVal = parseFloat(step1Data[i].pension_insurance);
|
||
if (pensionVal === 0) {
|
||
// 下方を探して最初の非ゼロ値を見つける
|
||
let nextNonZero = 0;
|
||
for (let j = i + 1; j < step1Data.length; j++) {
|
||
const nextVal = parseFloat(step1Data[j].pension_insurance);
|
||
if (nextVal > 0) {
|
||
nextNonZero = nextVal;
|
||
break;
|
||
}
|
||
}
|
||
if (nextNonZero > 0) {
|
||
step1Data[i].pension_insurance = nextNonZero;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("=== ステップ1完了(下方探索)===");
|
||
console.log(
|
||
"step1Data:",
|
||
step1Data.map((d) => ({
|
||
grade: d.grade,
|
||
pension: d.pension_insurance,
|
||
}))
|
||
);
|
||
|
||
// ステップ2: 非ゼロ値を下方へ延続(前の有効値が0以下になったら、前の非ゼロ値を使用)
|
||
const adjustedData = [...step1Data];
|
||
let lastValidPension = 0;
|
||
|
||
for (let i = 0; i < adjustedData.length; i++) {
|
||
const pensionVal = parseFloat(adjustedData[i].pension_insurance);
|
||
|
||
if (pensionVal > 0) {
|
||
// 非ゼロ値を見つけたら、それを記録
|
||
lastValidPension = pensionVal;
|
||
} else if (pensionVal === 0 && lastValidPension > 0) {
|
||
// 0だが前に有効な値がある場合、それを使用
|
||
adjustedData[i].pension_insurance = lastValidPension;
|
||
}
|
||
}
|
||
|
||
console.log("=== ステップ2完了(下方延続)===");
|
||
console.log(
|
||
"adjustedData:",
|
||
adjustedData.map((d) => ({
|
||
grade: d.grade,
|
||
pension: d.pension_insurance,
|
||
}))
|
||
);
|
||
|
||
console.log("調整後のデータ(最初の3件):", adjustedData.slice(0, 3));
|
||
|
||
const thead = `
|
||
<thead>
|
||
<tr>
|
||
<th rowspan="3">等級</th>
|
||
<th rowspan="3">標準報酬月額</th>
|
||
<th rowspan="3">報酬月額 以上</th>
|
||
<th rowspan="3">報酬月額 未満</th>
|
||
<th colspan="2">健康保険料(介護保険なし)</th>
|
||
<th colspan="2">健康保険料(介護保険あり)</th>
|
||
<th colspan="2">厚生年金保険料</th>
|
||
</tr>
|
||
<tr style="background-color: #f0f0f0;">
|
||
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
|
||
${
|
||
rates.healthNoCareRate !== undefined
|
||
? rates.healthNoCareRate.toFixed(2) + "%"
|
||
: "-"
|
||
}
|
||
</th>
|
||
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
|
||
${
|
||
rates.healthWithCareRate !== undefined
|
||
? rates.healthWithCareRate.toFixed(2) + "%"
|
||
: "-"
|
||
}
|
||
</th>
|
||
<th colspan="2" style="text-align: center; font-weight: bold; color: #007bff;">
|
||
${
|
||
rates.pensionRate !== undefined
|
||
? rates.pensionRate.toFixed(2) + "%"
|
||
: "-"
|
||
}
|
||
</th>
|
||
</tr>
|
||
<tr>
|
||
<th>全額</th>
|
||
<th>折半額</th>
|
||
<th>全額</th>
|
||
<th>折半額</th>
|
||
<th>全額</th>
|
||
<th>折半額</th>
|
||
</tr>
|
||
</thead>
|
||
`;
|
||
const tbody = `
|
||
<tbody>
|
||
${adjustedData
|
||
.map((row) => {
|
||
// 保険料を2で割って折半額を計算
|
||
const healthNoCareHalf = (
|
||
parseFloat(row.health_insurance_no_care) / 2
|
||
).toFixed(2);
|
||
const healthWithCareHalf = (
|
||
parseFloat(row.health_insurance_with_care) / 2
|
||
).toFixed(2);
|
||
const pensionHalf = (
|
||
parseFloat(row.pension_insurance) / 2
|
||
).toFixed(2);
|
||
|
||
return `
|
||
<tr>
|
||
<td>${row.grade ?? ""}</td>
|
||
<td>${Number(row.monthly_amount ?? 0).toLocaleString()}</td>
|
||
<td>${Number(row.salary_from ?? 0).toLocaleString()}</td>
|
||
<td>${Number(row.salary_to ?? 0).toLocaleString()}</td>
|
||
<td>${Number(
|
||
row.health_insurance_no_care ?? 0
|
||
).toLocaleString()}</td>
|
||
<td>${Number(healthNoCareHalf ?? 0).toLocaleString()}</td>
|
||
<td>${Number(
|
||
row.health_insurance_with_care ?? 0
|
||
).toLocaleString()}</td>
|
||
<td>${Number(healthWithCareHalf ?? 0).toLocaleString()}</td>
|
||
<td>${Number(
|
||
row.pension_insurance ?? 0
|
||
).toLocaleString()}</td>
|
||
<td>${Number(pensionHalf ?? 0).toLocaleString()}</td>
|
||
</tr>
|
||
`;
|
||
})
|
||
.join("")}
|
||
</tbody>
|
||
`;
|
||
return (
|
||
sourceHtml +
|
||
metadataHtml +
|
||
`<table style="width:100%; border-collapse: collapse; border: 1px solid #ccc;">${thead}${tbody}</table>`
|
||
);
|
||
}
|
||
|
||
async function loadStdRemunerationData() {
|
||
// 现在这个函数用于在导入后刷新年份列表
|
||
await loadStdRemunerationYears();
|
||
}
|
||
|
||
async function deleteStdRemunerationYear(year) {
|
||
const confirmed = confirm(
|
||
`⚠️ 警告\n\n${year}年度の標準報酬月額表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
|
||
);
|
||
if (!confirmed) {
|
||
console.log(`${year}年度の削除がキャンセルされました`);
|
||
return;
|
||
}
|
||
|
||
console.log(`=== ${year}年度データ削除開始 ===`);
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/standard-remuneration/year/${year}`,
|
||
{ method: "DELETE" }
|
||
);
|
||
|
||
console.log("削除レスポンスステータス:", response.status);
|
||
const result = await response.json();
|
||
console.log("削除結果:", result);
|
||
|
||
if (response.ok) {
|
||
alert(
|
||
`✓ ${year}年度のデータ(${result.deleted_count}件)を削除しました`
|
||
);
|
||
console.log(`=== ${year}年度データ削除完了 ===`);
|
||
|
||
// 年份リストをリロード
|
||
await loadStdRemunerationYears();
|
||
} else {
|
||
alert(
|
||
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("削除エラー:", error);
|
||
alert(`${year}年度のデータ削除に失敗しました:\n` + error.message);
|
||
}
|
||
}
|
||
|
||
async function clearAllStdRemunerationData() {
|
||
const confirmed = confirm(
|
||
"⚠️ 警告\n\n標準報酬月額表のすべてのデータを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?"
|
||
);
|
||
if (!confirmed) {
|
||
console.log("削除操作がキャンセルされました");
|
||
return;
|
||
}
|
||
|
||
console.log("=== 全データ削除開始 ===");
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/standard-remuneration/all`,
|
||
{ method: "DELETE" }
|
||
);
|
||
|
||
console.log("削除レスポンスステータス:", response.status);
|
||
const result = await response.json();
|
||
console.log("削除結果:", result);
|
||
|
||
if (response.ok) {
|
||
alert(
|
||
`✓ ${result.deleted_count}件のデータを削除しました\n\n再度Excelファイルをインポートしてください`
|
||
);
|
||
console.log("=== 全データ削除完了 ===");
|
||
|
||
// 年份列表をクリア
|
||
document.getElementById("stdRemunerationYearsList").innerHTML =
|
||
"<p>データが削除されました。Excelファイルを再度インポートしてください。</p>";
|
||
} else {
|
||
alert(
|
||
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("削除エラー:", error);
|
||
alert("全データ削除中にエラーが発生しました:\n" + error.message);
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 保険料率関連
|
||
// =====================================================
|
||
function showInsuranceForm() {
|
||
const el = document.getElementById("insuranceForm");
|
||
el.style.display = "block";
|
||
// 表示エリアへスクロールして最初の入力にフォーカス
|
||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
const firstInput = el.querySelector('[name="rate_year"]');
|
||
if (firstInput) firstInput.focus();
|
||
}
|
||
|
||
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="copyInsuranceRate(${
|
||
rate.rate_id
|
||
})">複製</button>
|
||
<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;
|
||
|
||
// フォームにデータを入力
|
||
fillInsuranceForm(rate);
|
||
|
||
// フォームを表示
|
||
const el = document.getElementById("insuranceForm");
|
||
el.style.display = "block";
|
||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
const firstInput = el.querySelector('[name="rate_year"]');
|
||
if (firstInput) firstInput.focus();
|
||
} catch (error) {
|
||
alert("データの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function copyInsuranceRate(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 = "false";
|
||
document.getElementById("editRateId").value = "";
|
||
|
||
// フォームにデータを入力
|
||
fillInsuranceForm(rate);
|
||
|
||
// フォームを表示
|
||
const el = document.getElementById("insuranceForm");
|
||
el.style.display = "block";
|
||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||
const firstInput = el.querySelector('[name="rate_year"]');
|
||
if (firstInput) firstInput.focus();
|
||
} catch (error) {
|
||
alert("データの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function fillInsuranceForm(rate) {
|
||
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 || "";
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 所得税率表関連
|
||
// =====================================================
|
||
|
||
// =====================================================
|
||
// 所得税率表関連
|
||
// =====================================================
|
||
let currentExpandedYear = null;
|
||
let currentExpandedStdYear = null;
|
||
|
||
async function loadIncomeTax() {
|
||
await loadIncomeTaxYears();
|
||
}
|
||
|
||
async function loadIncomeTaxYears() {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/income-tax/years`
|
||
);
|
||
const data = await response.json();
|
||
|
||
if (!data.years || data.years.length === 0) {
|
||
document.getElementById("taxYearsList").innerHTML =
|
||
"<p>データがありません。</p>";
|
||
return;
|
||
}
|
||
|
||
const html = data.years
|
||
.map(
|
||
(year) => `
|
||
<div class="year-item" id="year-${year}">
|
||
<div class="year-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div style="cursor: pointer; flex: 1;" onclick="toggleYear(${year})">
|
||
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deleteTaxDataYear(${year})" style="margin-left: 10px;">
|
||
削除
|
||
</button>
|
||
</div>
|
||
<div class="year-data" id="data-${year}">
|
||
<div class="filter-controls">
|
||
<label>扶養人数でフィルター: </label>
|
||
<select id="filter-${year}" onchange="loadYearData(${year})" onclick="event.stopPropagation()">
|
||
<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>
|
||
<option value="6">6人</option>
|
||
<option value="7">7人</option>
|
||
</select>
|
||
</div>
|
||
<div id="table-${year}">読み込み中...</div>
|
||
</div>
|
||
</div>
|
||
`
|
||
)
|
||
.join("");
|
||
|
||
document.getElementById("taxYearsList").innerHTML = html;
|
||
} catch (error) {
|
||
alert("年度リストの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function deleteTaxDataYear(year) {
|
||
const confirmed = confirm(
|
||
`⚠️ 警告\n\n${year}年度の所得税率表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
|
||
);
|
||
if (!confirmed) {
|
||
console.log(`${year}年度の削除がキャンセルされました`);
|
||
return;
|
||
}
|
||
|
||
console.log(`=== ${year}年度所得税データ削除開始 ===`);
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/income-tax/year/${year}`,
|
||
{ method: "DELETE" }
|
||
);
|
||
|
||
console.log("削除レスポンスステータス:", response.status);
|
||
const result = await response.json();
|
||
console.log("削除結果:", result);
|
||
|
||
if (response.ok) {
|
||
alert(
|
||
`✓ ${year}年度のデータ(${result.deleted_count}件)を削除しました`
|
||
);
|
||
console.log(`=== ${year}年度所得税データ削除完了 ===`);
|
||
|
||
// 年份リストをリロード
|
||
await loadIncomeTaxYears();
|
||
} else {
|
||
alert(
|
||
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error("削除エラー:", error);
|
||
alert(`${year}年度のデータ削除に失敗しました:\n` + error.message);
|
||
}
|
||
}
|
||
|
||
async function toggleYear(year) {
|
||
const yearItem = document.getElementById(`year-${year}`);
|
||
const wasExpanded = yearItem.classList.contains("expanded");
|
||
|
||
// 他の年度を閉じる
|
||
document.querySelectorAll(".year-item").forEach((item) => {
|
||
item.classList.remove("expanded");
|
||
});
|
||
|
||
if (!wasExpanded) {
|
||
yearItem.classList.add("expanded");
|
||
currentExpandedYear = year;
|
||
await loadYearData(year);
|
||
} else {
|
||
currentExpandedYear = null;
|
||
}
|
||
}
|
||
|
||
async function loadYearData(year) {
|
||
const filterSelect = document.getElementById(`filter-${year}`);
|
||
const dependentsFilter = filterSelect ? filterSelect.value : "";
|
||
const params = new URLSearchParams({ tax_year: year });
|
||
if (dependentsFilter) {
|
||
params.append("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(`table-${year}`).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(`table-${year}`).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}`);
|
||
loadIncomeTaxYears();
|
||
} else {
|
||
const error = await response.json();
|
||
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
|
||
}
|
||
} catch (error) {
|
||
alert("インポートに失敗しました");
|
||
console.error(error);
|
||
}
|
||
|
||
event.target.value = "";
|
||
}
|
||
|
||
// =====================================================
|
||
// 標準報酬月額表関連
|
||
// =====================================================
|
||
async function importStdRemunerationTable(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
// 都道府県をプロンプトで取得
|
||
const prefecture = prompt(
|
||
"都道府県を入力してください(例: 東京、神奈川):",
|
||
""
|
||
);
|
||
if (!prefecture || !prefecture.trim()) {
|
||
alert("都道府県を入力してください");
|
||
event.target.value = "";
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
formData.append("prefecture", prefecture.trim());
|
||
|
||
console.log("=== インポート開始 ===");
|
||
console.log("ファイル:", file.name);
|
||
console.log("都道府県:", prefecture.trim());
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/standard-remuneration/import`,
|
||
{
|
||
method: "POST",
|
||
body: formData,
|
||
}
|
||
);
|
||
|
||
console.log("レスポンスステータス:", response.status);
|
||
const result = await response.json();
|
||
console.log("レスポンスボディ:", result);
|
||
|
||
if (response.ok) {
|
||
let message = `✓ インポート成功!\n年度: ${result.year}年\n都道府県: ${prefecture}\nインポート件数: ${result.imported_count}件`;
|
||
if (result.source) {
|
||
message += `\n取得元: ${result.source}`;
|
||
}
|
||
console.log("インポート成功:", message);
|
||
console.log("デバッグ情報:", result.debug_info);
|
||
|
||
// メタデータをセッションストレージに保存
|
||
if (result.metadata) {
|
||
console.log("メタデータ保存:", result.metadata);
|
||
sessionStorage.setItem(
|
||
"stdRemunMetadata",
|
||
JSON.stringify(result.metadata)
|
||
);
|
||
}
|
||
|
||
alert(message);
|
||
// 年度リストをリロード(新UIは検索条件がないため、直接リロード)
|
||
loadStdRemunerationData();
|
||
} else {
|
||
// エラーレスポンスの詳細を表示
|
||
const errorDetail = result.detail;
|
||
console.error("インポート失敗:", errorDetail);
|
||
|
||
// エラー詳細が配列またはオブジェクトの場合
|
||
let errorMessage = "インポートに失敗しました:\n\n";
|
||
if (Array.isArray(errorDetail)) {
|
||
errorMessage += errorDetail.slice(0, 10).join("\n");
|
||
} else if (typeof errorDetail === "object") {
|
||
errorMessage += JSON.stringify(errorDetail, null, 2).substring(
|
||
0,
|
||
1000
|
||
);
|
||
} else {
|
||
errorMessage += String(errorDetail).substring(0, 500);
|
||
}
|
||
|
||
console.error("詳細エラー:", errorMessage);
|
||
alert(errorMessage);
|
||
}
|
||
} catch (error) {
|
||
console.error("例外発生:", error);
|
||
alert("インポートに失敗しました: " + error.message);
|
||
}
|
||
|
||
event.target.value = "";
|
||
}
|
||
|
||
// =====================================================
|
||
// 初期化
|
||
// =====================================================
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
loadInsuranceRates();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|