This commit is contained in:
admin
2026-01-21 10:58:19 +09:00
parent 8a00de8f03
commit 86020ada5c
36 changed files with 3417 additions and 363 deletions

View File

@@ -114,10 +114,57 @@
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">
@@ -129,6 +176,9 @@
</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>
<!-- 保険料率タブ -->
@@ -459,11 +509,22 @@
<div class="form-row">
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" required />
<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" />
<input
type="date"
name="valid_to"
min="1900-01-01"
max="2099-12-31"
/>
</div>
</div>
@@ -487,18 +548,6 @@
<!-- 所得税率表タブ -->
<div id="tab-tax" class="tab-content">
<h2>所得税率表</h2>
<div class="form-group" style="max-width: 300px">
<label>扶養人数でフィルター</label>
<select id="filterDependents" onchange="loadIncomeTax()">
<option value="">全て</option>
<option value="0">0人</option>
<option value="1">1人</option>
<option value="2">2人</option>
<option value="3">3人</option>
<option value="4">4人</option>
<option value="5">5人以上</option>
</select>
</div>
<button
class="btn btn-primary"
onclick="document.getElementById('csvFile').click()"
@@ -512,7 +561,40 @@
style="display: none"
onchange="importTaxTable(event)"
/>
<div id="taxList"></div>
<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>標準報酬月額表</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 id="stdRemunerationDataContainer" style="margin-top: 20px"></div>
</div>
</div>
@@ -535,6 +617,7 @@
"child-support": 1,
limits: 2,
tax: 3,
"standard-remuneration": 4,
};
document
@@ -551,6 +634,8 @@
loadLimitSettings();
} else if (tabName === "tax") {
loadIncomeTax();
} else if (tabName === "standard-remuneration") {
loadStdRemunerationData();
}
}
@@ -1189,21 +1274,95 @@
// =====================================================
// 所得税率表関連
// =====================================================
let currentExpandedYear = null;
async function loadIncomeTax() {
const dependentsFilter =
document.getElementById("filterDependents").value;
const params = dependentsFilter
? `?dependents_count=${dependentsFilter}`
: "";
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}" onclick="toggleYear(${year})">
<div class="year-header">
📅 ${year}年度 <span style="font-size: 14px; color: #666;">(クリックして展開)</span>
</div>
<div class="year-data" id="data-${year}">
<div class="filter-controls">
<label>扶養人数でフィルター: </label>
<select id="filter-${year}" onchange="loadYearData(${year})">
<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 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}`
`${API_BASE}/payroll/settings/income-tax?${params}`
);
const taxes = await response.json();
if (taxes.length === 0) {
document.getElementById("taxList").innerHTML =
document.getElementById(`table-${year}`).innerHTML =
"<p>データがありません。</p>";
return;
}
@@ -1223,27 +1382,25 @@
${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>
`
<tr>
<td>${tax.dependents_count}人</td>
<td>¥${Number(
tax.monthly_income_from
).toLocaleString()}</td>
<td>¥${Number(tax.monthly_income_to).toLocaleString()}</td>
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
<td>${tax.valid_from} ${tax.valid_to || "現在"}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
`;
document.getElementById("taxList").innerHTML = html;
document.getElementById(`table-${year}`).innerHTML = html;
} catch (error) {
alert("所得税率表の取得に失敗しました");
alert("データの取得に失敗しました");
console.error(error);
}
}
@@ -1274,7 +1431,7 @@
if (response.ok) {
const result = await response.json();
alert(result.message + `\n年度: ${result.year}`);
loadIncomeTax();
loadIncomeTaxYears();
} else {
const error = await response.json();
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
@@ -1287,6 +1444,224 @@
event.target.value = "";
}
// =====================================================
// 標準報酬月額表関連の関数
// =====================================================
async function loadStdRemunerationData() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/years`
);
if (!response.ok) {
throw new Error("Failed to load years");
}
const years = await response.json();
if (!years || years.length === 0) {
document.getElementById("stdRemunerationYearsList").innerHTML =
"<p>データがありません。Excelファイルをインポートしてください。</p>";
return;
}
const html = years
.map(
(year) => `
<div class="year-item" id="std-year-${year}">
<div class="year-header" onclick="toggleStdRemunerationYear(${year})">
<span class="year-title">${year}年度</span>
<span class="expand-icon">▼</span>
</div>
<div class="year-content" id="std-content-${year}"></div>
</div>
`
)
.join("");
document.getElementById("stdRemunerationYearsList").innerHTML = html;
} catch (error) {
alert("年度リストの取得に失敗しました");
console.error(error);
}
}
async function toggleStdRemunerationYear(year) {
const yearItem = document.getElementById(`std-year-${year}`);
const wasExpanded = yearItem.classList.contains("expanded");
// 他の年度を閉じる
document.querySelectorAll(".year-item").forEach((item) => {
item.classList.remove("expanded");
});
if (!wasExpanded) {
yearItem.classList.add("expanded");
await loadStdRemunerationYearData(year);
} else {
document.getElementById(`std-content-${year}`).innerHTML = "";
}
}
async function loadStdRemunerationYearData(year) {
const filterSelect = document.getElementById(`std-filter-${year}`);
const prefectureFilter = filterSelect ? filterSelect.value : "";
const params = new URLSearchParams({ year: year });
if (prefectureFilter) {
params.append("prefecture", prefectureFilter);
}
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration?${params}`
);
const data = await response.json();
if (data.length === 0) {
document.getElementById(`std-content-${year}`).innerHTML =
'<p class="no-data">データがありません</p>';
return;
}
// 都道府県でグループ化
const groupedByPrefecture = {};
data.forEach((row) => {
if (!groupedByPrefecture[row.prefecture]) {
groupedByPrefecture[row.prefecture] = [];
}
groupedByPrefecture[row.prefecture].push(row);
});
// 都道府県一覧を取得
const prefectures = Object.keys(groupedByPrefecture).sort();
const html = `
<div class="filter-section">
<label>都道府県で絞り込み:</label>
<select id="std-filter-${year}" onchange="loadStdRemunerationYearData(${year})" style="margin-left: 10px">
<option value="">すべて</option>
${prefectures
.map(
(pref) =>
`<option value="${pref}" ${
prefectureFilter === pref ? "selected" : ""
}>${pref}</option>`
)
.join("")}
</select>
</div>
${Object.entries(groupedByPrefecture)
.map(
([prefecture, rows]) => `
<div style="margin-top: 20px">
<h4>${prefecture}</h4>
<table>
<thead>
<tr>
<th rowspan="2">等級</th>
<th rowspan="2">標準報酬月額</th>
<th colspan="2">報酬月額</th>
<th colspan="2">健康保険料</th>
<th rowspan="2">厚生年金保険料</th>
</tr>
<tr>
<th>以上</th>
<th>未満</th>
<th>介護保険なし</th>
<th>介護保険あり</th>
</tr>
</thead>
<tbody>
${rows
.map(
(row) => `
<tr>
<td>${row.grade}</td>
<td class="number">${formatNumber(
row.monthly_amount
)}</td>
<td class="number">${formatNumber(row.salary_from)}</td>
<td class="number">${
row.salary_to ? formatNumber(row.salary_to) : "〜"
}</td>
<td class="number">${formatNumber(
row.health_insurance_no_care
)}</td>
<td class="number">${formatNumber(
row.health_insurance_with_care
)}</td>
<td class="number">${formatNumber(
row.pension_insurance
)}</td>
</tr>
`
)
.join("")}
</tbody>
</table>
</div>
`
)
.join("")}
`;
document.getElementById(`std-content-${year}`).innerHTML = html;
} catch (error) {
alert("データの取得に失敗しました");
console.error(error);
}
}
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());
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/import`,
{
method: "POST",
body: formData,
}
);
if (response.ok) {
const result = await response.json();
alert(
`インポートしました: ${result.imported_count}\n年度: ${
result.year
}\n都道府県: ${prefecture}\n\n取得元: ${result.source || ""}`
);
loadStdRemunerationData();
} else {
const error = await response.json();
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
}
} catch (error) {
alert("インポートに失敗しました");
console.error(error);
}
event.target.value = "";
}
function formatNumber(num) {
if (num === null || num === undefined) return "-";
return Number(num).toLocaleString("ja-JP");
}
// =====================================================
// 初期化
// =====================================================