Files
njts-accounting-core/frontend/payroll-settings.html
2026-02-07 20:04:42 +09:00

2053 lines
72 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" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 設定</title>
<link rel="stylesheet" href="/css/style.css" />
<style>
.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;
}
/* Add alternating row background for readability */
table tbody tr:nth-child(odd) {
background: #ffffff;
}
table tbody tr:nth-child(even) {
background: #f8f9fb;
}
.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">
<button
onclick="location.href = 'payroll.html'"
class="back-button"
style="
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-bottom: 20px;
"
>
← 給与管理メニューに戻る
</button>
<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 = "";
// =====================================================
// タブ切り替え
// =====================================================
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>