修改
This commit is contained in:
@@ -547,7 +547,7 @@
|
||||
|
||||
<!-- 所得税率表タブ -->
|
||||
<div id="tab-tax" class="tab-content">
|
||||
<h2>所得税率表</h2>
|
||||
<h2>所得税率表(年度1月~12月)</h2>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="document.getElementById('csvFile').click()"
|
||||
@@ -567,13 +567,13 @@
|
||||
|
||||
<!-- 標準報酬月額表タブ -->
|
||||
<div id="tab-standard-remuneration" class="tab-content">
|
||||
<h2>標準報酬月額表</h2>
|
||||
<h2>標準報酬月額表(年度3月~翌年2月)</h2>
|
||||
<div class="info-box">
|
||||
<strong>標準報酬月額表について:</strong>
|
||||
<ul style="margin: 10px 0 0 20px">
|
||||
<li>健康保険・厚生年金保険の保険料額表をインポートします</li>
|
||||
<li>都道府県ごとの料率表に対応しています</li>
|
||||
<li>年度と都道府県を指定してください</li>
|
||||
<li>年度をクリックして展開してください</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -594,7 +594,6 @@
|
||||
</div>
|
||||
|
||||
<div id="stdRemunerationYearsList" style="margin-top: 20px"></div>
|
||||
<div id="stdRemunerationDataContainer" style="margin-top: 20px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -635,7 +634,399 @@
|
||||
} else if (tabName === "tax") {
|
||||
loadIncomeTax();
|
||||
} else if (tabName === "standard-remuneration") {
|
||||
loadStdRemunerationData();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,7 +1034,12 @@
|
||||
// 保険料率関連
|
||||
// =====================================================
|
||||
function showInsuranceForm() {
|
||||
document.getElementById("insuranceForm").style.display = "block";
|
||||
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() {
|
||||
@@ -719,6 +1115,9 @@
|
||||
}</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>
|
||||
@@ -761,30 +1160,72 @@
|
||||
document.getElementById("editRateId").value = rateId;
|
||||
|
||||
// フォームにデータを入力
|
||||
const form = document.getElementById("insuranceRateForm");
|
||||
form.querySelector('[name="rate_year"]').value = rate.rate_year;
|
||||
form.querySelector('[name="calculation_target"]').value =
|
||||
rate.calculation_target;
|
||||
form.querySelector('[name="rate_type"]').value = rate.rate_type;
|
||||
form.querySelector('[name="prefecture"]').value = rate.prefecture;
|
||||
form.querySelector('[name="employee_rate"]').value = (
|
||||
rate.employee_rate * 100
|
||||
).toFixed(3);
|
||||
form.querySelector('[name="employer_rate"]').value = (
|
||||
rate.employer_rate * 100
|
||||
).toFixed(3);
|
||||
form.querySelector('[name="care_insurance_age_threshold"]').value =
|
||||
rate.care_insurance_age_threshold || "";
|
||||
form.querySelector('[name="notes"]').value = rate.notes || "";
|
||||
fillInsuranceForm(rate);
|
||||
|
||||
// フォームを表示
|
||||
document.getElementById("insuranceForm").style.display = "block";
|
||||
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;
|
||||
@@ -1275,6 +1716,7 @@
|
||||
// 所得税率表関連
|
||||
// =====================================================
|
||||
let currentExpandedYear = null;
|
||||
let currentExpandedStdYear = null;
|
||||
|
||||
async function loadIncomeTax() {
|
||||
await loadIncomeTaxYears();
|
||||
@@ -1296,14 +1738,19 @@
|
||||
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 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})">
|
||||
<select id="filter-${year}" onchange="loadYearData(${year})" onclick="event.stopPropagation()">
|
||||
<option value="">全て</option>
|
||||
<option value="0">0人</option>
|
||||
<option value="1">1人</option>
|
||||
@@ -1329,6 +1776,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -1445,181 +1932,19 @@
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 標準報酬月額表関連の関数
|
||||
// 標準報酬月額表関連
|
||||
// =====================================================
|
||||
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("都道府県名を入力してください");
|
||||
alert("都道府県を入力してください");
|
||||
event.target.value = "";
|
||||
return;
|
||||
}
|
||||
@@ -1628,6 +1953,10 @@
|
||||
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`,
|
||||
@@ -1637,31 +1966,59 @@
|
||||
}
|
||||
);
|
||||
|
||||
console.log("レスポンスステータス:", response.status);
|
||||
const result = await response.json();
|
||||
console.log("レスポンスボディ:", result);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
alert(
|
||||
`インポートしました: ${result.imported_count}件\n年度: ${
|
||||
result.year
|
||||
}年\n都道府県: ${prefecture}\n\n取得元: ${result.source || ""}`
|
||||
);
|
||||
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 error = await response.json();
|
||||
alert("インポートに失敗しました: " + JSON.stringify(error.detail));
|
||||
// エラーレスポンスの詳細を表示
|
||||
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) {
|
||||
alert("インポートに失敗しました");
|
||||
console.error(error);
|
||||
console.error("例外発生:", error);
|
||||
alert("インポートに失敗しました: " + error.message);
|
||||
}
|
||||
|
||||
event.target.value = "";
|
||||
}
|
||||
|
||||
function formatNumber(num) {
|
||||
if (num === null || num === undefined) return "-";
|
||||
return Number(num).toLocaleString("ja-JP");
|
||||
}
|
||||
|
||||
// =====================================================
|
||||
// 初期化
|
||||
// =====================================================
|
||||
|
||||
Reference in New Issue
Block a user