399 lines
12 KiB
HTML
399 lines
12 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;
|
||
}
|
||
.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 {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
}
|
||
.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;
|
||
}
|
||
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;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>給与管理 - 設定</h1>
|
||
|
||
<div class="nav-tabs">
|
||
<button class="nav-tab active" onclick="showTab('insurance')">
|
||
保険料率
|
||
</button>
|
||
<button class="nav-tab" onclick="showTab('tax')">所得税率表</button>
|
||
</div>
|
||
|
||
<!-- 保険料率タブ -->
|
||
<div id="tab-insurance" class="tab-content active">
|
||
<h2>社会保険料率設定</h2>
|
||
<button class="btn btn-primary" onclick="showInsuranceForm()">
|
||
新規追加
|
||
</button>
|
||
<div id="insuranceList"></div>
|
||
|
||
<div
|
||
id="insuranceForm"
|
||
style="
|
||
display: none;
|
||
margin-top: 20px;
|
||
padding: 20px;
|
||
border: 2px solid #007bff;
|
||
border-radius: 5px;
|
||
"
|
||
>
|
||
<h3>保険料率の登録</h3>
|
||
<form onsubmit="saveInsuranceRate(event)">
|
||
<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>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>従業員負担率 (%) *</label>
|
||
<input
|
||
type="number"
|
||
name="employee_rate"
|
||
step="0.0001"
|
||
required
|
||
placeholder="例: 5.0 (5%)"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>会社負担率 (%) *</label>
|
||
<input
|
||
type="number"
|
||
name="employer_rate"
|
||
step="0.0001"
|
||
required
|
||
placeholder="例: 5.0 (5%)"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>適用開始日 *</label>
|
||
<input type="date" name="valid_from" required />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>適用終了日</label>
|
||
<input type="date" name="valid_to" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>備考</label>
|
||
<input type="text" name="notes" />
|
||
</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-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()"
|
||
>
|
||
CSVインポート
|
||
</button>
|
||
<input
|
||
type="file"
|
||
id="csvFile"
|
||
accept=".csv"
|
||
style="display: none"
|
||
onchange="importTaxTable(event)"
|
||
/>
|
||
<div id="taxList"></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"));
|
||
|
||
if (tabName === "insurance") {
|
||
document.querySelectorAll(".nav-tab")[0].classList.add("active");
|
||
document.getElementById("tab-insurance").classList.add("active");
|
||
loadInsuranceRates();
|
||
} else if (tabName === "tax") {
|
||
document.querySelectorAll(".nav-tab")[1].classList.add("active");
|
||
document.getElementById("tab-tax").classList.add("active");
|
||
loadIncomeTax();
|
||
}
|
||
}
|
||
|
||
function showInsuranceForm() {
|
||
document.getElementById("insuranceForm").style.display = "block";
|
||
}
|
||
|
||
function hideInsuranceForm() {
|
||
document.getElementById("insuranceForm").style.display = "none";
|
||
document.querySelector("#insuranceForm form").reset();
|
||
}
|
||
|
||
async function loadInsuranceRates() {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/insurance-rates`,
|
||
);
|
||
const rates = await response.json();
|
||
|
||
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_type}</td>
|
||
<td>${(parseFloat(rate.employee_rate) * 100).toFixed(2)}%</td>
|
||
<td>${(parseFloat(rate.employer_rate) * 100).toFixed(2)}%</td>
|
||
<td>${rate.valid_from} ~ ${rate.valid_to || "現在"}</td>
|
||
<td>${rate.notes || "-"}</td>
|
||
</tr>
|
||
`,
|
||
)
|
||
.join("")}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById("insuranceList").innerHTML = html;
|
||
} catch (error) {
|
||
alert("保険料率の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function saveInsuranceRate(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
const data = Object.fromEntries(formData.entries());
|
||
|
||
// パーセントを小数に変換
|
||
data.employee_rate = parseFloat(data.employee_rate) / 100;
|
||
data.employer_rate = parseFloat(data.employer_rate) / 100;
|
||
|
||
// 空の値を除外
|
||
if (!data.valid_to) delete data.valid_to;
|
||
if (!data.notes) delete data.notes;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/insurance-rates`,
|
||
{
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("保険料率を登録しました");
|
||
hideInsuranceForm();
|
||
loadInsuranceRates();
|
||
} else {
|
||
const error = await response.json();
|
||
alert("登録に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("登録に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function loadIncomeTax() {
|
||
const dependentsFilter =
|
||
document.getElementById("filterDependents").value;
|
||
const params = dependentsFilter
|
||
? `?dependents_count=${dependentsFilter}`
|
||
: "";
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/income-tax${params}`,
|
||
);
|
||
const taxes = await response.json();
|
||
|
||
const html = `
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>扶養人数</th>
|
||
<th>月収From</th>
|
||
<th>月収To</th>
|
||
<th>税額</th>
|
||
<th>適用期間</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${taxes
|
||
.map(
|
||
(tax) => `
|
||
<tr>
|
||
<td>${tax.dependents_count}人</td>
|
||
<td>¥${Number(tax.monthly_income_from).toLocaleString()}</td>
|
||
<td>¥${Number(tax.monthly_income_to).toLocaleString()}</td>
|
||
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
|
||
<td>${tax.valid_from} ~ ${tax.valid_to || "現在"}</td>
|
||
</tr>
|
||
`,
|
||
)
|
||
.join("")}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById("taxList").innerHTML = html;
|
||
} catch (error) {
|
||
alert("所得税率表の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function importTaxTable(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
const formData = new FormData();
|
||
formData.append("file", file);
|
||
|
||
const validFrom = prompt(
|
||
"適用開始日を入力してください (YYYY-MM-DD):",
|
||
new Date().toISOString().split("T")[0],
|
||
);
|
||
if (!validFrom) return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/income-tax/import?valid_from=${validFrom}`,
|
||
{
|
||
method: "POST",
|
||
body: formData,
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
alert(result.message);
|
||
loadIncomeTax();
|
||
} else {
|
||
const error = await response.json();
|
||
alert("インポートに失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("インポートに失敗しました");
|
||
console.error(error);
|
||
}
|
||
|
||
event.target.value = "";
|
||
}
|
||
|
||
// 初期化
|
||
loadInsuranceRates();
|
||
</script>
|
||
</body>
|
||
</html>
|