541 lines
18 KiB
HTML
541 lines
18 KiB
HTML
<!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" />
|
||
<link rel="stylesheet" href="/css/mobile.css" />
|
||
<style>
|
||
.employee-select {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #f9f9f9;
|
||
border-radius: 5px;
|
||
}
|
||
.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;
|
||
}
|
||
.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>
|
||
|
||
<button class="btn btn-primary" onclick="showAddForm()">
|
||
新規設定追加
|
||
</button>
|
||
|
||
<div id="settingsList" style="margin-top: 20px"></div>
|
||
|
||
<div
|
||
id="addForm"
|
||
style="
|
||
display: none;
|
||
margin-top: 20px;
|
||
padding: 20px;
|
||
border: 2px solid #007bff;
|
||
border-radius: 5px;
|
||
"
|
||
>
|
||
<h3 id="formTitle">給与設定の追加</h3>
|
||
<form onsubmit="saveSalarySetting(event)">
|
||
<input type="hidden" name="setting_id" id="setting_id" />
|
||
|
||
<div class="form-group">
|
||
<label>従業員 *</label>
|
||
<select name="employee_id" id="employee_id" required>
|
||
<option value="">選択してください</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>基本給 (円) *</label>
|
||
<input
|
||
type="number"
|
||
name="base_salary"
|
||
id="base_salary"
|
||
step="0.01"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>時給 (円)</label>
|
||
<input
|
||
type="number"
|
||
name="hourly_rate"
|
||
id="hourly_rate"
|
||
step="0.01"
|
||
/>
|
||
<small>時給制の場合に入力してください</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>雇用形態 *</label>
|
||
<select name="employment_type" id="employment_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>
|
||
<select name="payment_type" id="payment_type" required>
|
||
<option value="">選択してください</option>
|
||
<option value="月給">月給</option>
|
||
<option value="時給">時給</option>
|
||
<option value="日給">日給</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>通勤手当 (円)</label>
|
||
<input
|
||
type="number"
|
||
name="commute_allowance"
|
||
id="commute_allowance"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他手当 (円)</label>
|
||
<input
|
||
type="number"
|
||
name="other_allowance"
|
||
id="other_allowance"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="display: flex; align-items: center">
|
||
<input
|
||
type="checkbox"
|
||
name="employment_insurance_eligible"
|
||
id="employment_insurance_eligible"
|
||
checked
|
||
style="width: auto; margin-right: 8px"
|
||
/>
|
||
<span>雇用保険加入対象</span>
|
||
</label>
|
||
<small>チェックを入れると雇用保険に加入します</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>適用開始日 *</label>
|
||
<input
|
||
type="date"
|
||
name="valid_from"
|
||
id="valid_from"
|
||
required
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>適用終了日</label>
|
||
<input
|
||
type="date"
|
||
name="valid_to"
|
||
id="valid_to"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
<small>通常は空欄のままにしてください</small>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="hideAddForm()"
|
||
>
|
||
キャンセル
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE = "";
|
||
let employees = [];
|
||
|
||
async function loadEmployees() {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/employees/?is_active=true`,
|
||
);
|
||
employees = await response.json();
|
||
|
||
const select = document.getElementById("employee_id");
|
||
select.innerHTML =
|
||
'<option value="">選択してください</option>' +
|
||
employees
|
||
.map(
|
||
(emp) =>
|
||
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
|
||
)
|
||
.join("");
|
||
|
||
loadAllSalarySettings();
|
||
} catch (error) {
|
||
alert("従業員一覧の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function loadAllSalarySettings() {
|
||
try {
|
||
// すべての従業員の給与設定を取得
|
||
const allSettings = [];
|
||
for (const emp of employees) {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/salary/${emp.employee_id}/current`,
|
||
);
|
||
if (response.ok) {
|
||
const setting = await response.json();
|
||
allSettings.push({
|
||
...setting,
|
||
employee_code: emp.employee_code,
|
||
employee_name: emp.name,
|
||
employee_id: emp.employee_id,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
`従業員 ${emp.employee_id} の給与設定取得失敗`,
|
||
error,
|
||
);
|
||
}
|
||
}
|
||
|
||
displayAllSettings(allSettings);
|
||
} catch (error) {
|
||
alert("給与設定の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function displayAllSettings(settings) {
|
||
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>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${
|
||
settings.length > 0
|
||
? settings
|
||
.map(
|
||
(s) => `
|
||
<tr>
|
||
<td>${s.employee_code}</td>
|
||
<td>${s.employee_name}</td>
|
||
<td>¥${Number(s.base_salary).toLocaleString()}</td>
|
||
<td>${
|
||
s.hourly_rate
|
||
? "¥" + Number(s.hourly_rate).toLocaleString()
|
||
: "-"
|
||
}</td>
|
||
<td>${s.employment_type}</td>
|
||
<td>${s.payment_type}</td>
|
||
<td>¥${Number(s.commute_allowance).toLocaleString()}</td>
|
||
<td>¥${Number(s.other_allowance).toLocaleString()}</td>
|
||
<td>${
|
||
s.employment_insurance_eligible !== false
|
||
? "✓ 対象"
|
||
: "✗ 非対象"
|
||
}</td>
|
||
<td>${s.valid_from} ~ ${s.valid_to || "現在"}</td>
|
||
<td style="white-space:nowrap;">
|
||
<button class="btn btn-primary" onclick="editSetting(${s.setting_id})" style="padding: 5px 10px; font-size: 12px;">編集</button>
|
||
<button class="btn" onclick="deleteSetting(${s.setting_id}, '${s.employee_name}', '${s.valid_from}')" style="padding: 5px 10px; font-size: 12px; background:#dc3545; color:white; margin-left:4px;">削除</button>
|
||
</td>
|
||
</tr>
|
||
`,
|
||
)
|
||
.join("")
|
||
: '<tr><td colspan="11" style="text-align:center;">給与設定が登録されていません</td></tr>'
|
||
}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
document.getElementById("settingsList").innerHTML = html;
|
||
}
|
||
|
||
async function editSetting(settingId) {
|
||
try {
|
||
// 設定IDから詳細を取得する必要がありますが、現在のAPIでは難しいので
|
||
// 全従業員の設定を取得して該当するものを探す
|
||
for (const emp of employees) {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/salary/${emp.employee_id}`,
|
||
);
|
||
const settings = await response.json();
|
||
const setting = settings.find((s) => s.setting_id === settingId);
|
||
|
||
if (setting) {
|
||
// フォームに値を設定
|
||
document.getElementById("formTitle").textContent =
|
||
"給与設定の編集";
|
||
document.getElementById("setting_id").value = setting.setting_id;
|
||
document.getElementById("employee_id").value = emp.employee_id;
|
||
document.getElementById("employee_id").disabled = true; // 従業員は変更不可
|
||
document.getElementById("base_salary").value =
|
||
setting.base_salary;
|
||
document.getElementById("hourly_rate").value =
|
||
setting.hourly_rate || "";
|
||
document.getElementById("employment_type").value =
|
||
setting.employment_type;
|
||
document.getElementById("payment_type").value =
|
||
setting.payment_type;
|
||
document.getElementById("commute_allowance").value =
|
||
setting.commute_allowance;
|
||
document.getElementById("other_allowance").value =
|
||
setting.other_allowance;
|
||
document.getElementById("employment_insurance_eligible").checked =
|
||
setting.employment_insurance_eligible !== false;
|
||
document.getElementById("valid_from").value = setting.valid_from;
|
||
document.getElementById("valid_to").value =
|
||
setting.valid_to || "";
|
||
|
||
document.getElementById("addForm").style.display = "block";
|
||
return;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
alert("給与設定の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function deleteSetting(settingId, employeeName, validFrom) {
|
||
if (
|
||
!confirm(
|
||
`「${employeeName}」の適用開始日 ${validFrom} の給与設定を削除しますか?\nこの操作は元に戻せません。`,
|
||
)
|
||
)
|
||
return;
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/settings/salary/${settingId}`,
|
||
{
|
||
method: "DELETE",
|
||
},
|
||
);
|
||
if (response.ok || response.status === 204) {
|
||
alert("給与設定を削除しました");
|
||
loadAllSalarySettings();
|
||
} else {
|
||
let errMsg = `HTTP ${response.status}`;
|
||
try {
|
||
const errBody = await response.json();
|
||
if (errBody.detail) errMsg = String(errBody.detail);
|
||
} catch (_) {}
|
||
alert("削除に失敗しました: " + errMsg);
|
||
}
|
||
} catch (error) {
|
||
alert("削除に失敗しました: " + (error.message || error));
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function showAddForm() {
|
||
document.getElementById("formTitle").textContent = "給与設定の追加";
|
||
document.getElementById("addForm").style.display = "block";
|
||
document.querySelector("#addForm form").reset();
|
||
document.getElementById("setting_id").value = "";
|
||
document.getElementById("employee_id").disabled = false;
|
||
// デフォルト値設定
|
||
document.getElementById("valid_from").value = new Date()
|
||
.toISOString()
|
||
.split("T")[0];
|
||
document.getElementById("commute_allowance").value = "0";
|
||
document.getElementById("other_allowance").value = "0";
|
||
}
|
||
|
||
function hideAddForm() {
|
||
document.getElementById("addForm").style.display = "none";
|
||
document.querySelector("#addForm form").reset();
|
||
document.getElementById("employee_id").disabled = false;
|
||
}
|
||
|
||
async function saveSalarySetting(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
const settingId = document.getElementById("setting_id").value;
|
||
|
||
// disabled な employee_id は FormData に含まれないため DOM から直接取得
|
||
const employeeIdRaw =
|
||
formData.get("employee_id") ||
|
||
document.getElementById("employee_id").value;
|
||
const employeeId = parseInt(employeeIdRaw);
|
||
|
||
if (!settingId && (!employeeId || isNaN(employeeId))) {
|
||
alert("従業員を選択してください");
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
base_salary: parseFloat(formData.get("base_salary")) || 0,
|
||
employment_type: formData.get("employment_type"),
|
||
payment_type: formData.get("payment_type"),
|
||
commute_allowance: parseFloat(formData.get("commute_allowance")) || 0,
|
||
other_allowance: parseFloat(formData.get("other_allowance")) || 0,
|
||
employment_insurance_eligible: document.getElementById(
|
||
"employment_insurance_eligible",
|
||
).checked,
|
||
valid_from: formData.get("valid_from"),
|
||
};
|
||
|
||
// 新規登録のみ employee_id を含める
|
||
if (!settingId) {
|
||
data.employee_id = employeeId;
|
||
}
|
||
|
||
if (formData.get("hourly_rate")) {
|
||
data.hourly_rate = parseFloat(formData.get("hourly_rate"));
|
||
}
|
||
|
||
if (formData.get("valid_to")) {
|
||
data.valid_to = formData.get("valid_to");
|
||
}
|
||
|
||
try {
|
||
let response;
|
||
if (settingId) {
|
||
// 更新
|
||
response = await fetch(
|
||
`${API_BASE}/payroll/settings/salary/${settingId}`,
|
||
{
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
},
|
||
);
|
||
} else {
|
||
// 新規追加
|
||
response = await fetch(`${API_BASE}/payroll/settings/salary`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
if (response.ok) {
|
||
alert(
|
||
settingId ? "給与設定を更新しました" : "給与設定を保存しました",
|
||
);
|
||
hideAddForm();
|
||
loadAllSalarySettings();
|
||
} else {
|
||
let errMsg = `HTTP ${response.status}`;
|
||
try {
|
||
const errBody = await response.json();
|
||
const detail = errBody.detail;
|
||
if (Array.isArray(detail)) {
|
||
errMsg = detail
|
||
.map((e) => e.msg || JSON.stringify(e))
|
||
.join(", ");
|
||
} else if (detail) {
|
||
errMsg = String(detail);
|
||
}
|
||
} catch (_) {
|
||
errMsg += ` ${await response.text().catch(() => "")}`;
|
||
}
|
||
alert("保存に失敗しました: " + errMsg);
|
||
}
|
||
} catch (error) {
|
||
alert("保存に失敗しました: " + (error.message || error));
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// 初期化
|
||
loadEmployees();
|
||
</script>
|
||
</body>
|
||
</html>
|