Files
njts-accounting-core/frontend/payroll-employees.html
2026-01-22 21:05:48 +09:00

1567 lines
59 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" />
<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;
}
.employee-list {
margin-top: 20px;
}
.employee-item {
padding: 15px;
border: 1px solid #ddd;
margin-bottom: 10px;
border-radius: 5px;
cursor: pointer;
}
.employee-item:hover {
background: #f9f9f9;
}
.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;
}
.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;
}
.dependent-list {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 5px;
}
.dependent-item {
padding: 10px;
border: 1px solid #ddd;
margin-bottom: 10px;
background: white;
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">
<button class="nav-tab active" onclick="showTab('list')">
従業員一覧
</button>
<button class="nav-tab" onclick="showTab('create')">新規登録</button>
<button
class="nav-tab"
onclick="showTab('detail')"
id="detailTab"
style="display: none"
>
従業員詳細
</button>
<button
class="nav-tab"
onclick="showTab('edit')"
id="editTab"
style="display: none"
>
従業員編集
</button>
</div>
<!-- 従業員一覧タブ -->
<div id="tab-list" class="tab-content active">
<div class="filters">
<label>
<input
type="checkbox"
id="filterActive"
checked
onchange="loadEmployees()"
/>
在職中のみ表示
</label>
</div>
<div id="employeeList" class="employee-list"></div>
</div>
<!-- 新規登録タブ -->
<div id="tab-create" class="tab-content">
<h2>従業員の新規登録</h2>
<form id="createForm" onsubmit="createEmployee(event)">
<div class="form-group">
<label>従業員コード *</label>
<input
type="text"
name="employee_code"
required
placeholder="例: 20260001"
/>
<small style="color: #666"
>形式: 年+4桁の連番例: 2026年入社の1番目は20260001</small
>
</div>
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" required />
</div>
<div class="form-group">
<label>フリガナ</label>
<input type="text" name="name_kana" />
</div>
<div class="form-group">
<label>生年月日 *</label>
<input
type="date"
name="birth_date"
required
min="1900-01-01"
max="2026-12-31"
/>
<small style="color: #666"
>介護保険料の計算に使用されます40歳以上が対象</small
>
</div>
<div class="form-group">
<label>性別</label>
<select name="gender">
<option value="">選択してください</option>
<option value="男性">男性</option>
<option value="女性">女性</option>
<option value="その他">その他</option>
</select>
</div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input
type="text"
name="my_number"
pattern="\d{12}"
maxlength="12"
placeholder="12桁の数字"
/>
<small style="color: #666"
>12桁の数字を入力してください機密情報として管理されます</small
>
</div>
<div class="form-group">
<label>メールアドレス</label>
<input type="email" name="email" />
</div>
<div class="form-group">
<label>電話番号</label>
<input type="tel" name="phone" />
</div>
<div class="form-group">
<label>入社日 *</label>
<input
type="date"
name="hire_date"
required
min="1900-01-01"
max="2099-12-31"
/>
</div>
<div class="form-group">
<label>郵便番号</label>
<input type="text" name="postal_code" placeholder="例: 123-4567" />
</div>
<div class="form-group">
<label>住所</label>
<input type="text" name="address" />
</div>
<div class="form-group">
<label>銀行名</label>
<input type="text" name="bank_name" />
</div>
<div class="form-group">
<label>支店名</label>
<input type="text" name="bank_branch" />
</div>
<div class="form-group">
<label>口座番号</label>
<input type="text" name="bank_account_number" />
</div>
<div class="form-group">
<label>口座種別</label>
<select name="bank_account_type">
<option value="">選択してください</option>
<option value="普通">普通</option>
<option value="当座">当座</option>
</select>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" rows="3"></textarea>
</div>
<h3
style="
margin-top: 30px;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
"
>
📸 写真・身分証明書
</h3>
<div class="form-group">
<label>本人写真</label>
<input
type="file"
name="photo"
accept="image/*"
onchange="previewImage(event, 'photoPreview')"
/>
<div id="photoPreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>マイナンバーカード</label>
<input
type="file"
name="my_number_card_image"
accept="image/*"
onchange="previewImage(event, 'myNumberPreview')"
/>
<div id="myNumberPreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>在留カード</label>
<input
type="file"
name="residence_card_image"
accept="image/*"
onchange="previewImage(event, 'residencePreview')"
/>
<div id="residencePreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>健康保険証</label>
<input
type="file"
name="health_insurance_card_image"
accept="image/*"
onchange="previewImage(event, 'healthInsurancePreview')"
/>
<div id="healthInsurancePreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>その他の身分証明書</label>
<input
type="file"
name="other_id_image"
accept="image/*"
onchange="previewImage(event, 'otherIdPreview')"
/>
<div id="otherIdPreview" style="margin-top: 10px"></div>
</div>
<button type="submit" class="btn btn-primary">登録</button>
<button
type="button"
class="btn btn-secondary"
onclick="showTab('list')"
>
キャンセル
</button>
</form>
</div>
<!-- 従業員詳細タブ -->
<div id="tab-detail" class="tab-content">
<div id="employeeDetail"></div>
</div>
<!-- 従業員編集タブ -->
<div id="tab-edit" class="tab-content">
<h2>従業員情報の編集</h2>
<form id="editForm" onsubmit="updateEmployee(event)">
<input type="hidden" name="employee_id" id="edit_employee_id" />
<div class="form-group">
<label>従業員コード *</label>
<input
type="text"
name="employee_code"
id="edit_employee_code"
required
placeholder="例: 20260001"
/>
<small style="color: #666"
>形式: 年+4桁の連番例: 2026年入社の1番目は20260001</small
>
</div>
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" id="edit_name" required />
</div>
<div class="form-group">
<label>フリガナ</label>
<input type="text" name="name_kana" id="edit_name_kana" />
</div>
<div class="form-group">
<label>生年月日 *</label>
<input
type="date"
name="birth_date"
id="edit_birth_date"
required
min="1900-01-01"
max="2026-12-31"
/>
<small style="color: #666"
>介護保険料の計算に使用されます40歳以上が対象</small
>
</div>
<div class="form-group">
<label>性別</label>
<select name="gender" id="edit_gender">
<option value="">選択してください</option>
<option value="男性">男性</option>
<option value="女性">女性</option>
<option value="その他">その他</option>
</select>
</div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input
type="text"
name="my_number"
id="edit_my_number"
pattern="\d{12}"
maxlength="12"
placeholder="12桁の数字"
/>
<small style="color: #666"
>12桁の数字を入力してください機密情報として管理されます</small
>
</div>
<div class="form-group">
<label>メールアドレス</label>
<input type="email" name="email" id="edit_email" />
</div>
<div class="form-group">
<label>電話番号</label>
<input type="tel" name="phone" id="edit_phone" />
</div>
<div class="form-group">
<label>入社日 *</label>
<input
type="date"
name="hire_date"
id="edit_hire_date"
required
min="1900-01-01"
max="2099-12-31"
/>
</div>
<div class="form-group">
<label>郵便番号</label>
<input
type="text"
name="postal_code"
id="edit_postal_code"
placeholder="例: 123-4567"
/>
</div>
<div class="form-group">
<label>住所</label>
<input type="text" name="address" id="edit_address" />
</div>
<div class="form-group">
<label>銀行名</label>
<input type="text" name="bank_name" id="edit_bank_name" />
</div>
<div class="form-group">
<label>支店名</label>
<input type="text" name="bank_branch" id="edit_bank_branch" />
</div>
<div class="form-group">
<label>口座番号</label>
<input
type="text"
name="bank_account_number"
id="edit_bank_account_number"
/>
</div>
<div class="form-group">
<label>口座種別</label>
<select name="bank_account_type" id="edit_bank_account_type">
<option value="">選択してください</option>
<option value="普通">普通</option>
<option value="当座">当座</option>
</select>
</div>
<div class="form-group">
<label>備考</label>
<textarea name="notes" id="edit_notes" rows="3"></textarea>
</div>
<h3
style="
margin-top: 30px;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
"
>
📸 写真・身分証明書
</h3>
<div class="form-group">
<label>本人写真</label>
<div id="edit_photo_current"></div>
<input
type="file"
name="photo"
accept="image/*"
onchange="previewImage(event, 'edit_photoPreview')"
/>
<div id="edit_photoPreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>マイナンバーカード</label>
<div id="edit_my_number_card_current"></div>
<input
type="file"
name="my_number_card_image"
accept="image/*"
onchange="previewImage(event, 'edit_myNumberPreview')"
/>
<div id="edit_myNumberPreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>在留カード</label>
<div id="edit_residence_card_current"></div>
<input
type="file"
name="residence_card_image"
accept="image/*"
onchange="previewImage(event, 'edit_residencePreview')"
/>
<div id="edit_residencePreview" style="margin-top: 10px"></div>
</div>
<div class="form-group">
<label>健康保険証</label>
<div id="edit_health_insurance_current"></div>
<input
type="file"
name="health_insurance_card_image"
accept="image/*"
onchange="previewImage(event, 'edit_healthInsurancePreview')"
/>
<div
id="edit_healthInsurancePreview"
style="margin-top: 10px"
></div>
</div>
<div class="form-group">
<label>その他の身分証明書</label>
<div id="edit_other_id_current"></div>
<input
type="file"
name="other_id_image"
accept="image/*"
onchange="previewImage(event, 'edit_otherIdPreview')"
/>
<div id="edit_otherIdPreview" style="margin-top: 10px"></div>
</div>
<button type="submit" class="btn btn-primary">更新</button>
<button
type="button"
class="btn btn-secondary"
onclick="closeDetailTab()"
>
キャンセル
</button>
</form>
</div>
</div>
<script>
const API_BASE = "";
let currentEmployee = null;
// 年齢計算関数
function calculateAge(birthDate) {
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birth.getDate())
) {
age--;
}
return age;
}
// 画像プレビュー関数
function previewImage(event, previewId) {
const file = event.target.files[0];
const preview = document.getElementById(previewId);
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
preview.innerHTML = `<img src="${e.target.result}" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />`;
};
reader.readAsDataURL(file);
} else {
preview.innerHTML = "";
}
}
// ファイルをBase64に変換旧方式 - 使用しない)
// function fileToBase64(file) {
// return new Promise((resolve, reject) => {
// const reader = new FileReader();
// reader.onload = () => resolve(reader.result);
// reader.onerror = reject;
// reader.readAsDataURL(file);
// });
// }
// ファイルをサーバーにアップロード(新方式)
async function uploadFileToServer(
file,
employeeCode,
fileType = "image"
) {
const formData = new FormData();
formData.append("file", file);
formData.append("file_type", fileType);
try {
const response = await fetch(
`${API_BASE}/files/upload/employee/${employeeCode}`,
{
method: "POST",
body: formData,
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "アップロードに失敗しました");
}
const result = await response.json();
return result.file_path; // サーバーから返されたファイルパス
} catch (error) {
console.error("ファイルアップロードエラー:", error);
throw error;
}
}
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 === "list") {
document.querySelector(".nav-tab").classList.add("active");
document.getElementById("tab-list").classList.add("active");
loadEmployees();
} else if (tabName === "create") {
document.querySelectorAll(".nav-tab")[1].classList.add("active");
document.getElementById("tab-create").classList.add("active");
} else if (tabName === "detail") {
document.getElementById("detailTab").classList.add("active");
document.getElementById("tab-detail").classList.add("active");
} else if (tabName === "edit") {
document.getElementById("editTab").classList.add("active");
document.getElementById("tab-edit").classList.add("active");
}
}
// タブを閉じる関数
function closeDetailTab() {
document.getElementById("detailTab").style.display = "none";
document.getElementById("editTab").style.display = "none";
showTab("list");
}
async function loadEmployees() {
const isActive = document.getElementById("filterActive").checked;
const url = `${API_BASE}/payroll/employees/${
isActive ? "?is_active=true" : ""
}`;
try {
const response = await fetch(url);
const employees = await response.json();
const html = employees
.map(
(emp) => `
<div class="employee-item" onclick="viewEmployee(${
emp.employee_id
})">
<strong>${emp.employee_code}</strong> - ${emp.name}
${emp.name_kana ? `(${emp.name_kana})` : ""}
<br>
<small>入社日: ${emp.hire_date} ${
emp.termination_date ? ` | 退職日: ${emp.termination_date}` : ""
}</small>
</div>
`
)
.join("");
document.getElementById("employeeList").innerHTML =
html || "<p>従業員が登録されていません</p>";
} catch (error) {
alert("従業員一覧の取得に失敗しました");
console.error(error);
}
}
async function createEmployee(event) {
event.preventDefault();
const form = event.target;
try {
// まず従業員コードを取得
const employeeCode = form.employee_code.value;
// 写真ファイルをアップロード
const photoUploads = {};
const photoFields = [
{ name: "photo", field: "photo_url" },
{ name: "my_number_card_image", field: "my_number_card_image_url" },
{ name: "residence_card_image", field: "residence_card_image_url" },
{
name: "health_insurance_card_image",
field: "health_insurance_card_image_url",
},
{ name: "other_id_image", field: "other_id_image_url" },
];
for (const { name, field } of photoFields) {
const fileInput = form[name];
if (fileInput.files[0]) {
try {
const filePath = await uploadFileToServer(
fileInput.files[0],
employeeCode,
"image"
);
photoUploads[field] = filePath;
} catch (error) {
alert(`${name}のアップロードに失敗しました: ${error.message}`);
return;
}
}
}
// 従業員データを作成
const data = {
employee_code: employeeCode,
name: form.name.value,
name_kana: form.name_kana.value || null,
birth_date: form.birth_date.value,
gender: form.gender.value || null,
my_number: form.my_number.value || null,
email: form.email.value || null,
phone: form.phone.value || null,
hire_date: form.hire_date.value,
postal_code: form.postal_code.value || null,
address: form.address.value || null,
bank_name: form.bank_name.value || null,
bank_branch: form.bank_branch.value || null,
bank_account_number: form.bank_account_number.value || null,
bank_account_type: form.bank_account_type.value || null,
notes: form.notes.value || null,
...photoUploads, // アップロードされたファイルパスを追加
};
const response = await fetch(`${API_BASE}/payroll/employees/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
alert("従業員を登録しました");
form.reset();
// プレビューをクリア
[
"photoPreview",
"myNumberPreview",
"residencePreview",
"healthInsurancePreview",
"otherIdPreview",
].forEach((id) => {
document.getElementById(id).innerHTML = "";
});
showTab("list");
} else {
const error = await response.json();
alert("登録に失敗しました: " + error.detail);
}
} catch (error) {
alert("登録に失敗しました");
console.error(error);
}
}
async function viewEmployee(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}`
);
const employee = await response.json();
currentEmployee = employee;
const html = `
<h2>${employee.name} (${employee.employee_code})</h2>
<p><strong>フリガナ:</strong> ${
employee.name_kana || "-"
}</p>
<p><strong>生年月日:</strong> ${
employee.birth_date || "-"
} ${
employee.birth_date
? `(${calculateAge(employee.birth_date)}歳)`
: ""
}</p>
<p><strong>性別:</strong> ${employee.gender || "-"}</p>
<p><strong>マイナンバー:</strong> ${
employee.my_number
? `${employee.my_number.substring(
0,
4
)}-${employee.my_number.substring(
4,
8
)}-${employee.my_number.substring(8, 12)}`
: "-"
}</p>
<p><strong>メール:</strong> ${employee.email || "-"}</p>
<p><strong>電話:</strong> ${employee.phone || "-"}</p>
<p><strong>郵便番号:</strong> ${
employee.postal_code || "-"
}</p>
<p><strong>住所:</strong> ${employee.address || "-"}</p>
<p><strong>入社日:</strong> ${employee.hire_date}</p>
<p><strong>退職日:</strong> ${
employee.termination_date || "-"
}</p>
<p><strong>銀行:</strong> ${employee.bank_name || "-"} ${
employee.bank_branch || ""
}</p>
<p><strong>口座:</strong> ${
employee.bank_account_type || ""
} ${employee.bank_account_number || "-"}</p>
<p><strong>備考:</strong> ${employee.notes || "-"}</p>
<h3 style="margin-top: 30px; border-bottom: 2px solid #007bff; padding-bottom: 10px;">📸 写真・身分証明書</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px;">
${
employee.photo_url
? `
<div>
<strong>本人写真</strong><br>
<img src="${
employee.photo_url.startsWith("data:")
? employee.photo_url
: API_BASE + employee.photo_url
}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
</div>
`
: ""
}
${
employee.my_number_card_image_url
? `
<div>
<strong>マイナンバーカード</strong><br>
<img src="${
employee.my_number_card_image_url.startsWith(
"data:"
)
? employee.my_number_card_image_url
: API_BASE + employee.my_number_card_image_url
}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
</div>
`
: ""
}
${
employee.residence_card_image_url
? `
<div>
<strong>在留カード</strong><br>
<img src="${
employee.residence_card_image_url.startsWith(
"data:"
)
? employee.residence_card_image_url
: API_BASE + employee.residence_card_image_url
}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
</div>
`
: ""
}
${
employee.health_insurance_card_image_url
? `
<div>
<strong>健康保険証</strong><br>
<img src="${
employee.health_insurance_card_image_url.startsWith(
"data:"
)
? employee.health_insurance_card_image_url
: API_BASE +
employee.health_insurance_card_image_url
}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
</div>
`
: ""
}
${
employee.other_id_image_url
? `
<div>
<strong>その他の身分証明書</strong><br>
<img src="${
employee.other_id_image_url.startsWith("data:")
? employee.other_id_image_url
: API_BASE + employee.other_id_image_url
}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
</div>
`
: ""
}
</div>
<div class="dependent-list">
<h3>扶養家族 <button class="btn btn-primary" onclick="showAddDependentForm(${
employee.employee_id
})" style="padding: 5px 10px; font-size: 12px;">扶養家族追加</button></h3>
${(() => {
// 源泉税控除対象人数を計算16歳以上
let koureiCount = 0;
if (
employee.dependents &&
employee.dependents.length > 0
) {
koureiCount = employee.dependents.filter((dep) => {
const taxStatus = getTaxDeductionStatus(
dep.birth_date
);
// 源泉税控除対象は16歳以上一般扶養親族16-18歳、特定扶養親族19-22歳、老人扶養親族70歳以上
return (
taxStatus?.isDeductible &&
(taxStatus.type === "一般扶養親族" ||
taxStatus.type === "控除対象扶養親族" ||
taxStatus.type === "老人扶養親族")
);
}).length;
}
return `
<div style="background: #f0f8ff; padding: 10px; border-radius: 5px; margin-bottom: 15px;">
<strong style="color: #0066cc;">甲欄扶養親族人数源泉税控除対象: ${koureiCount}</strong>
<small style="display: block; margin-top: 5px; color: #666;">
16歳以上の扶養親族が対象一般扶養親族16-18特定扶養親族19-22老人扶養親族70歳以上
</small>
</div>
`;
})()}
${
employee.dependents && employee.dependents.length > 0
? employee.dependents
.map((dep) => {
const taxStatus = getTaxDeductionStatus(
dep.birth_date
);
return `
<div class="dependent-item">
<strong>${dep.name}</strong> (${
dep.relationship
})
${
dep.birth_date
? ` - 生年月日: ${dep.birth_date} (${taxStatus?.age}歳)`
: ""
}
${
taxStatus?.label
? ` <span style="color:${taxStatus.color}; font-weight:bold;">[${taxStatus.label}]</span>`
: ""
}
${
dep.is_spouse
? ' <span style="color:blue;">[配偶者]</span>'
: ""
}
${
dep.is_disabled
? ' <span style="color:red;">[障害者]</span>'
: ""
}
<br><small>有効期間: ${dep.valid_from} ${
dep.valid_to || "現在"
}</small>
<button class="btn btn-primary" onclick='editDependent(${JSON.stringify(
dep
)})' style="padding: 3px 8px; font-size: 11px; margin-left: 10px;">編集</button>
<button class="btn btn-secondary" onclick="deleteDependent(${
dep.dependent_id
})" style="padding: 3px 8px; font-size: 11px; margin-left: 5px;">削除</button>
</div>
`;
})
.join("")
: "<p>扶養家族が登録されていません</p>"
}
${(() => {
// 源泉税控除対象人数を再計算(説明用)
let koureiCount = 0;
if (
employee.dependents &&
employee.dependents.length > 0
) {
koureiCount = employee.dependents.filter((dep) => {
const taxStatus = getTaxDeductionStatus(
dep.birth_date
);
return (
taxStatus?.isDeductible &&
(taxStatus.type === "一般扶養親族" ||
taxStatus.type === "控除対象扶養親族" ||
taxStatus.type === "老人扶養親族")
);
}).length;
}
return `
<div style="margin-top: 15px; padding: 10px; background: #fff9e6; border-left: 4px solid #ffc107; border-radius: 4px;">
<strong>📋 源泉税控除の説明:</strong>
<ul style="margin: 10px 0 0 20px; font-size: 13px; line-height: 1.6;">
<li><strong style="color: purple;">老人扶養親族(70歳以上)</strong>: </li>
<li><strong style="color: green;">控除対象扶養親族(19歳以上23歳未満)</strong>: </li>
<li><strong style="color: orange;">一般扶養親族(16歳以上19歳未満)</strong>: </li>
<li><strong style="color: #999;">16歳未満</strong>: </li>
</ul>
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
給与計算時は<strong>甲欄扶養親族人数(${koureiCount})</strong>使
</p>
</div>
`;
})()}
</div>
<button class="btn btn-primary" onclick="editEmployee(${
employee.employee_id
})">編集</button>
<button class="btn btn-secondary" onclick="closeDetailTab()">閉じる</button>
`;
document.getElementById("employeeDetail").innerHTML = html;
document.getElementById("detailTab").style.display = "block";
showTab("detail");
} catch (error) {
alert("従業員情報の取得に失敗しました");
console.error(error);
}
}
// 従業員編集
async function editEmployee(employeeId) {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}`
);
const employee = await response.json();
currentEmployee = employee;
// 基本情報を設定
document.getElementById("edit_employee_id").value =
employee.employee_id;
document.getElementById("edit_employee_code").value =
employee.employee_code;
document.getElementById("edit_name").value = employee.name || "";
document.getElementById("edit_name_kana").value =
employee.name_kana || "";
document.getElementById("edit_birth_date").value =
employee.birth_date || "";
document.getElementById("edit_gender").value = employee.gender || "";
document.getElementById("edit_my_number").value =
employee.my_number || "";
document.getElementById("edit_email").value = employee.email || "";
document.getElementById("edit_phone").value = employee.phone || "";
document.getElementById("edit_hire_date").value =
employee.hire_date || "";
document.getElementById("edit_postal_code").value =
employee.postal_code || "";
document.getElementById("edit_address").value =
employee.address || "";
document.getElementById("edit_bank_name").value =
employee.bank_name || "";
document.getElementById("edit_bank_branch").value =
employee.bank_branch || "";
document.getElementById("edit_bank_account_number").value =
employee.bank_account_number || "";
document.getElementById("edit_bank_account_type").value =
employee.bank_account_type || "";
document.getElementById("edit_notes").value = employee.notes || "";
// 現在の写真を表示
const photoFields = [
{
field: "photo_url",
div: "edit_photo_current",
label: "現在の写真",
},
{
field: "my_number_card_image_url",
div: "edit_my_number_card_current",
label: "現在のマイナンバーカード",
},
{
field: "residence_card_image_url",
div: "edit_residence_card_current",
label: "現在の在留カード",
},
{
field: "health_insurance_card_image_url",
div: "edit_health_insurance_current",
label: "現在の健康保険証",
},
{
field: "other_id_image_url",
div: "edit_other_id_current",
label: "現在のその他ID",
},
];
photoFields.forEach(({ field, div, label }) => {
const divElement = document.getElementById(div);
if (employee[field]) {
// ファイルパスの場合は API_BASE を追加、Base64の場合はそのまま
const imageUrl = employee[field].startsWith("data:")
? employee[field]
: `${API_BASE}${employee[field]}`;
divElement.innerHTML = `
<strong>${label}:</strong><br>
<img src="${imageUrl}" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 5px;" />
`;
} else {
divElement.innerHTML = `<small style="color: #999;">登録されていません</small>`;
}
});
// プレビューをクリア
[
"edit_photoPreview",
"edit_myNumberPreview",
"edit_residencePreview",
"edit_healthInsurancePreview",
"edit_otherIdPreview",
].forEach((id) => {
document.getElementById(id).innerHTML = "";
});
// 編集タブを表示
document.getElementById("editTab").style.display = "block";
showTab("edit");
} catch (error) {
alert("従業員情報の取得に失敗しました");
console.error(error);
}
}
// 従業員更新
async function updateEmployee(event) {
event.preventDefault();
const form = event.target;
const employeeId = form.employee_id.value;
const employeeCode = form.employee_code.value;
try {
// 写真ファイルをアップロード
const photoUploads = {};
const photoFields = [
{ name: "photo", field: "photo_url" },
{ name: "my_number_card_image", field: "my_number_card_image_url" },
{ name: "residence_card_image", field: "residence_card_image_url" },
{
name: "health_insurance_card_image",
field: "health_insurance_card_image_url",
},
{ name: "other_id_image", field: "other_id_image_url" },
];
for (const { name, field } of photoFields) {
const fileInput = form[name];
if (fileInput.files[0]) {
try {
const filePath = await uploadFileToServer(
fileInput.files[0],
employeeCode,
"image"
);
photoUploads[field] = filePath;
} catch (error) {
alert(`${name}のアップロードに失敗しました: ${error.message}`);
return;
}
}
}
const data = {
employee_code: employeeCode,
name: form.name.value,
name_kana: form.name_kana.value || null,
birth_date: form.birth_date.value,
gender: form.gender.value || null,
my_number: form.my_number.value || null,
email: form.email.value || null,
phone: form.phone.value || null,
hire_date: form.hire_date.value,
postal_code: form.postal_code.value || null,
address: form.address.value || null,
bank_name: form.bank_name.value || null,
bank_branch: form.bank_branch.value || null,
bank_account_number: form.bank_account_number.value || null,
bank_account_type: form.bank_account_type.value || null,
notes: form.notes.value || null,
...photoUploads, // 新しくアップロードされたファイルパスのみ追加
};
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (response.ok) {
alert("従業員情報を更新しました");
viewEmployee(employeeId);
} else {
const error = await response.json();
alert(`更新に失敗しました: ${error.detail}`);
}
} catch (error) {
alert("更新中にエラーが発生しました");
console.error(error);
}
}
// 扶養家族追加フォームを表示
function showAddDependentForm(employeeId) {
const html = `
<div id="dependentFormModal" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 600px; max-height: 90vh; overflow-y: auto; background: white; padding: 30px; border: 2px solid #007bff; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 1000;">
<h2>扶養家族の追加</h2>
<form onsubmit="addDependent(event, ${employeeId})">
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" required />
</div>
<div class="form-group">
<label>続柄 *</label>
<select name="relationship" 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="date" name="birth_date" min="1900-01-01" max="2099-12-31" />
</div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input type="text" name="mynumber" placeholder="例123456789012" maxlength="12" />
<small>12桁の数字任意入力</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_spouse" value="true" />
配偶者控除対象
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_disabled" value="true" />
障害者控除対象
</label>
</div>
<div class="form-group">
<label>年間所得金額 (円)</label>
<input type="number" name="income_amount" step="0.01" value="0" />
<small>配偶者・扶養控除の判定に使用</small>
</div>
<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" />
<small>通常は空欄のままにしてください</small>
</div>
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('dependentFormModal').remove()">キャンセル</button>
</form>
</div>
`;
const modal = document.createElement("div");
modal.innerHTML = html;
document.body.appendChild(modal);
// デフォルト値設定
const validFromInput = modal.querySelector('[name="valid_from"]');
validFromInput.value = new Date().toISOString().split("T")[0];
}
// 扶養家族を追加
async function addDependent(event, employeeId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {
name: formData.get("name"),
relationship: formData.get("relationship"),
birth_date: formData.get("birth_date") || null,
mynumber: formData.get("mynumber") || null,
is_spouse: formData.get("is_spouse") === "true",
is_disabled: formData.get("is_disabled") === "true",
income_amount: parseFloat(formData.get("income_amount")) || 0,
valid_from: formData.get("valid_from"),
valid_to: formData.get("valid_to") || null,
};
try {
const response = await fetch(
`${API_BASE}/payroll/employees/${employeeId}/dependents`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (response.ok) {
alert("扶養家族を追加しました");
document.getElementById("dependentFormModal").remove();
viewEmployee(employeeId);
} else {
const error = await response.json();
alert("追加に失敗しました: " + error.detail);
}
} catch (error) {
alert("追加に失敗しました");
console.error(error);
}
}
// 現在時点の満年齢を計算
function calculateAgeAtYearEnd(birthDate) {
if (!birthDate) return null;
const today = new Date();
const birth = new Date(birthDate);
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birth.getDate())
) {
age--;
}
return age;
}
// 所得税・住民税控除対象の判定
function getTaxDeductionStatus(birthDate) {
const age = calculateAgeAtYearEnd(birthDate);
if (age === null) return null;
if (age >= 70) {
return {
isDeductible: true,
type: "老人扶養親族",
label: "老人扶養親族(源泉税・住民税)",
color: "purple",
age,
};
} else if (age >= 19) {
return {
isDeductible: true,
type: "控除対象扶養親族",
label: "控除対象(源泉税・住民税)",
color: "green",
age,
};
} else if (age >= 16) {
return {
isDeductible: true,
type: "一般扶養親族",
label: "一般扶養親族(源泉税・住民税)",
color: "orange",
age,
};
} else {
return {
isDeductible: false,
type: null,
label: "源泉税控除対象外",
color: null,
age,
};
}
}
// 扶養家族を削除
async function deleteDependent(dependentId) {
if (!confirm("この扶養家族を削除しますか?")) return;
try {
const response = await fetch(
`${API_BASE}/payroll/employees/dependents/${dependentId}`,
{
method: "DELETE",
}
);
if (response.ok) {
alert("扶養家族を削除しました");
viewEmployee(currentEmployee.employee_id);
} else {
const error = await response.json();
alert("削除に失敗しました: " + error.detail);
}
} catch (error) {
alert("削除に失敗しました");
console.error(error);
}
}
// 扶養家族を編集
function editDependent(dependent) {
const html = `
<div id="dependentFormModal" style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: 600px; max-height: 90vh; overflow-y: auto; background: white; padding: 30px; border: 2px solid #007bff; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); z-index: 1000;">
<h2>扶養家族の編集</h2>
<form onsubmit="updateDependent(event, ${dependent.dependent_id})">
<div class="form-group">
<label>氏名 *</label>
<input type="text" name="name" value="${
dependent.name
}" required />
</div>
<div class="form-group">
<label>続柄 *</label>
<select name="relationship" required>
<option value="">選択してください</option>
<option value="配偶者" ${
dependent.relationship === "配偶者" ? "selected" : ""
}>配偶者</option>
<option value="子" ${
dependent.relationship === "子" ? "selected" : ""
}>子</option>
<option value="父" ${
dependent.relationship === "父" ? "selected" : ""
}>父</option>
<option value="母" ${
dependent.relationship === "母" ? "selected" : ""
}>母</option>
<option value="その他" ${
dependent.relationship === "その他" ? "selected" : ""
}>その他</option>
</select>
</div>
<div class="form-group">
<label>生年月日</label>
<input type="date" name="birth_date" value="${
dependent.birth_date || ""
}" min="1900-01-01" max="2099-12-31" />
</div>
<div class="form-group">
<label>マイナンバーカード番号</label>
<input type="text" name="mynumber" value="${
dependent.mynumber || ""
}" placeholder="例123456789012" maxlength="12" />
<small>12桁の数字任意入力</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_spouse" value="true" ${
dependent.is_spouse ? "checked" : ""
} />
配偶者控除対象
</label>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_disabled" value="true" ${
dependent.is_disabled ? "checked" : ""
} />
障害者控除対象
</label>
</div>
<div class="form-group">
<label>年間所得金額 (円)</label>
<input type="number" name="income_amount" step="0.01" value="${
dependent.income_amount || 0
}" />
<small>配偶者・扶養控除の判定に使用</small>
</div>
<div class="form-group">
<label>適用開始日 *</label>
<input type="date" name="valid_from" value="${
dependent.valid_from
}" required min="1900-01-01" max="2099-12-31" />
</div>
<div class="form-group">
<label>適用終了日</label>
<input type="date" name="valid_to" value="${
dependent.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="document.getElementById('dependentFormModal').remove()">キャンセル</button>
</form>
</div>
`;
const modal = document.createElement("div");
modal.innerHTML = html;
document.body.appendChild(modal);
}
// 扶養家族を更新
async function updateDependent(event, dependentId) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const data = {
name: formData.get("name"),
relationship: formData.get("relationship"),
birth_date: formData.get("birth_date") || null,
mynumber: formData.get("mynumber") || null,
is_spouse: formData.get("is_spouse") === "true",
is_disabled: formData.get("is_disabled") === "true",
income_amount: parseFloat(formData.get("income_amount")) || 0,
valid_from: formData.get("valid_from"),
valid_to: formData.get("valid_to") || null,
};
try {
const response = await fetch(
`${API_BASE}/payroll/employees/dependents/${dependentId}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
);
if (response.ok) {
alert("扶養家族を更新しました");
document.getElementById("dependentFormModal").remove();
viewEmployee(currentEmployee.employee_id);
} else {
const error = await response.json();
alert("更新に失敗しました: " + error.detail);
}
} catch (error) {
alert("更新に失敗しました");
console.error(error);
}
}
// 初期化
loadEmployees();
</script>
</body>
</html>