diff --git a/backend/app/payroll/calculation/router.py b/backend/app/payroll/calculation/router.py index 17acbdd..700aa62 100644 --- a/backend/app/payroll/calculation/router.py +++ b/backend/app/payroll/calculation/router.py @@ -51,7 +51,9 @@ def calculate_payroll(request: schemas.PayrollCalculationRequest): payment_date=request.payment_date, other_allowance=request.other_allowance, resident_tax=request.resident_tax, - other_deduction=request.other_deduction + other_deduction=request.other_deduction, + calc_income_tax=request.calc_income_tax, + calc_social_insurance=request.calc_social_insurance ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -174,8 +176,10 @@ def update_payroll(payroll_id: int, request: schemas.MonthlyPayrollUpdate): @router.post("/{payroll_id}/recalculate", response_model=schemas.MonthlyPayroll) -def recalculate_payroll(payroll_id: int): +def recalculate_payroll(payroll_id: int, request: schemas.RecalculateRequest = None): """給与を再計算""" + if request is None: + request = schemas.RecalculateRequest() with get_connection() as conn: with conn.cursor() as cur: # 既存データを取得 @@ -204,7 +208,9 @@ def recalculate_payroll(payroll_id: int): other_allowance=existing["other_allowance"], resident_tax=existing["resident_tax"], other_deduction=existing["other_deduction"], - calculated_by="recalculated" + calculated_by="recalculated", + calc_income_tax=request.calc_income_tax, + calc_social_insurance=request.calc_social_insurance ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/app/payroll/calculation/schemas.py b/backend/app/payroll/calculation/schemas.py index 163c29f..eb60730 100644 --- a/backend/app/payroll/calculation/schemas.py +++ b/backend/app/payroll/calculation/schemas.py @@ -123,6 +123,14 @@ class PayrollCalculationRequest(BaseModel): other_allowance: Decimal = Decimal("0") resident_tax: Decimal = Decimal("0") other_deduction: Decimal = Decimal("0") + calc_income_tax: bool = True + calc_social_insurance: bool = True + + +class RecalculateRequest(BaseModel): + """再計算リクエスト""" + calc_income_tax: bool = True + calc_social_insurance: bool = True class PayrollApprovalRequest(BaseModel): diff --git a/backend/app/payroll/calculation/service.py b/backend/app/payroll/calculation/service.py index 64d9301..e93ea4f 100644 --- a/backend/app/payroll/calculation/service.py +++ b/backend/app/payroll/calculation/service.py @@ -231,7 +231,9 @@ class PayrollCalculationService: other_allowance: Decimal = Decimal("0"), resident_tax: Decimal = Decimal("0"), other_deduction: Decimal = Decimal("0"), - calculated_by: str = "system" + calculated_by: str = "system", + calc_income_tax: bool = True, + calc_social_insurance: bool = True ) -> Dict[str, Any]: """給与を計算""" @@ -378,7 +380,7 @@ class PayrollCalculationService: health_insurance = ( health_standard_salary * Decimal(str(health_insurance_rate["employee_rate"])) ).quantize(Decimal("0.01")) - else: + elif calc_social_insurance: errors.append(f"健康保険料率が見つかりません(年度: {payment_date.year})") # 介護保険(40歳以上65歳未満が対象、標準報酬月額表から検索した月額で計算) @@ -391,7 +393,7 @@ class PayrollCalculationService: ).quantize(Decimal("0.01")) print(f"[DEBUG] 介護保険計算: {health_standard_salary} × {care_insurance_rate['employee_rate']}% = {care_insurance}") else: - errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year})") + errors.append(f"介護保険料率が見つかりません(年度: {payment_date.year})") if calc_social_insurance else None # 厚生年金(上限適用後の標準報酬月額で計算) pension_insurance_rate = PayrollCalculationService.get_insurance_rate("厚生年金", payment_date) @@ -401,7 +403,7 @@ class PayrollCalculationService: pension_standard_salary * Decimal(str(pension_insurance_rate["employee_rate"])) ).quantize(Decimal("0.01")) print(f"[DEBUG] 厚生年金計算: {pension_standard_salary} × {pension_insurance_rate['employee_rate']}% = {pension_insurance}") - else: + elif calc_social_insurance: errors.append(f"厚生年金保険料率が見つかりません(年度: {payment_date.year})") # 雇用保険 @@ -417,9 +419,16 @@ class PayrollCalculationService: ).quantize(Decimal("0.01")) elif not is_employment_insurance_eligible: print(f"[DEBUG] この従業員は雇用保険非対象です") - elif not employment_insurance_rate: + elif not employment_insurance_rate and calc_social_insurance: errors.append(f"雇用保険料率が見つかりません(年度: {payment_date.year})") + # 社会保険計算スキップ時は全て。0にする + if not calc_social_insurance: + health_insurance = Decimal("0") + care_insurance = Decimal("0") + pension_insurance = Decimal("0") + employment_insurance = Decimal("0") + # エラーがある場合は、エラーメッセージを返す if errors: raise ValueError(f"給与計算に必要なデータが不足しています:\n" + "\n".join(errors)) @@ -441,11 +450,14 @@ class PayrollCalculationService: employment_insurance ) - income_tax = PayrollCalculationService.calculate_income_tax( - taxable_income, - dependents_count, - payment_date - ) + if calc_income_tax: + income_tax = PayrollCalculationService.calculate_income_tax( + taxable_income, + dependents_count, + payment_date + ) + else: + income_tax = Decimal("0") # 総控除額 total_deduction = ( diff --git a/backend/app/payroll/settings/router.py b/backend/app/payroll/settings/router.py index 51819a7..43fc4f0 100644 --- a/backend/app/payroll/settings/router.py +++ b/backend/app/payroll/settings/router.py @@ -12,6 +12,7 @@ import openpyxl import xlrd from . import schemas from ...core.database import get_connection +from psycopg.errors import UniqueViolation router = APIRouter(prefix="/payroll/settings", tags=["Payroll - Settings"]) @@ -67,45 +68,51 @@ def create_salary_setting(setting: schemas.SalarySettingCreate): ) has_employment_insurance_column = cur.fetchone() - if has_employment_insurance_column: - # カラムが存在する場合 - cur.execute( - """ - INSERT INTO salary_settings ( - employee_id, base_salary, hourly_rate, employment_type, - payment_type, commute_allowance, other_allowance, - employment_insurance_eligible, - valid_from, valid_to - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - RETURNING * - """, - ( - setting.employee_id, setting.base_salary, setting.hourly_rate, - setting.employment_type, setting.payment_type, - setting.commute_allowance, setting.other_allowance, - setting.employment_insurance_eligible, - setting.valid_from, setting.valid_to - ), + try: + if has_employment_insurance_column: + # カラムが存在する場合 + cur.execute( + """ + INSERT INTO salary_settings ( + employee_id, base_salary, hourly_rate, employment_type, + payment_type, commute_allowance, other_allowance, + employment_insurance_eligible, + valid_from, valid_to + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING * + """, + ( + setting.employee_id, setting.base_salary, setting.hourly_rate, + setting.employment_type, setting.payment_type, + setting.commute_allowance, setting.other_allowance, + setting.employment_insurance_eligible, + setting.valid_from, setting.valid_to + ), + ) + else: + # カラムが存在しない場合(互換性維持) + cur.execute( + """ + INSERT INTO salary_settings ( + employee_id, base_salary, hourly_rate, employment_type, + payment_type, commute_allowance, other_allowance, + valid_from, valid_to + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING * + """, + ( + setting.employee_id, setting.base_salary, setting.hourly_rate, + setting.employment_type, setting.payment_type, + setting.commute_allowance, setting.other_allowance, + setting.valid_from, setting.valid_to + ), + ) + print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました") + except UniqueViolation: + raise HTTPException( + status_code=409, + detail="この適用開始日はすでに登録されています。別の日付を指定してください。" ) - else: - # カラムが存在しない場合(互換性維持) - cur.execute( - """ - INSERT INTO salary_settings ( - employee_id, base_salary, hourly_rate, employment_type, - payment_type, commute_allowance, other_allowance, - valid_from, valid_to - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) - RETURNING * - """, - ( - setting.employee_id, setting.base_salary, setting.hourly_rate, - setting.employment_type, setting.payment_type, - setting.commute_allowance, setting.other_allowance, - setting.valid_from, setting.valid_to - ), - ) - print("[DEBUG] employment_insurance_eligible カラムがまだ存在しないため、除外しました") result = cur.fetchone() conn.commit() @@ -184,10 +191,16 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate) with get_connection() as conn: with conn.cursor() as cur: - cur.execute( - f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *", - values, - ) + try: + cur.execute( + f"UPDATE salary_settings SET {set_clause} WHERE setting_id = %s RETURNING *", + values, + ) + except UniqueViolation: + raise HTTPException( + status_code=409, + detail="この適用開始日はすでに登録されています。別の日付を指定してください。" + ) result = cur.fetchone() if not result: raise HTTPException(status_code=404, detail="給与設定が見つかりません") @@ -195,6 +208,21 @@ def update_salary_setting(setting_id: int, setting: schemas.SalarySettingUpdate) return result +@router.delete("/salary/{setting_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_salary_setting(setting_id: int): + """給与設定を削除""" + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "DELETE FROM salary_settings WHERE setting_id = %s RETURNING setting_id", + (setting_id,), + ) + result = cur.fetchone() + if not result: + raise HTTPException(status_code=404, detail="給与設定が見つかりません") + conn.commit() + + # ======================================== # 社会保険料率管理 # ======================================== diff --git a/backend/app/payroll/vouchers/schemas.py b/backend/app/payroll/vouchers/schemas.py index 7813ca0..1567af4 100644 --- a/backend/app/payroll/vouchers/schemas.py +++ b/backend/app/payroll/vouchers/schemas.py @@ -32,6 +32,7 @@ class VoucherDataSummary(BaseModel): employee_id: int employee_code: str employee_name: str + employment_type: Optional[str] = None has_salary_data: bool has_bonus_data: bool salary_count: int = 0 diff --git a/backend/app/payroll/vouchers/service.py b/backend/app/payroll/vouchers/service.py index 00225f5..cb04d24 100644 --- a/backend/app/payroll/vouchers/service.py +++ b/backend/app/payroll/vouchers/service.py @@ -310,6 +310,17 @@ def build_voucher_summary( emp_name = emp_row.get("name") else: emp_code, emp_name = emp_row + + # 雇用形態を最新の給与設定から取得 + cur.execute( + "SELECT employment_type FROM salary_settings WHERE employee_id = %s ORDER BY valid_from DESC LIMIT 1", + (emp_id,) + ) + ss_row = cur.fetchone() + if ss_row: + employment_type = ss_row.get("employment_type") if isinstance(ss_row, dict) else ss_row[0] + else: + employment_type = None except Exception as e: logger.error(f"従業員情報取得エラー: {str(e)}") continue @@ -333,6 +344,7 @@ def build_voucher_summary( employee_id=emp_id, employee_code=emp_code, employee_name=emp_name, + employment_type=employment_type, has_salary_data=len(salary_list) > 0, has_bonus_data=len(bonus_list) > 0, salary_count=len(salary_list), diff --git a/frontend/journal-edit.html b/frontend/journal-edit.html index be367ec..eeeeee8 100644 --- a/frontend/journal-edit.html +++ b/frontend/journal-edit.html @@ -129,6 +129,7 @@ 科目 借方 貸方 + 税率 摘要(行) 操作 @@ -141,6 +142,7 @@ 0 + @@ -159,6 +161,43 @@ const API = ""; let accounts = []; + // カスタムアラートモーダル + function showAlert(msg, type, callback) { + const configs = { + success: { + icon: "✔", + iconColor: "#22a84b", + bg: "#f0faf4", + borderTop: "#22a84b", + }, + error: { + icon: "✖", + iconColor: "#e53935", + bg: "#fff5f5", + borderTop: "#e53935", + }, + warning: { + icon: "⚠", + iconColor: "#e67e00", + bg: "#fffbf0", + borderTop: "#f0a500", + }, + }; + const c = configs[type] || configs.warning; + const overlay = document.getElementById("alertModalOverlay"); + document.getElementById("alertModalIcon").innerHTML = + `${c.icon}`; + document.getElementById("alertModalMsg").textContent = msg; + const box = document.getElementById("alertModalBox"); + box.style.borderTop = `5px solid ${c.borderTop}`; + box.style.background = c.bg; + overlay.style.display = "flex"; + document.getElementById("alertModalOkBtn").onclick = function () { + overlay.style.display = "none"; + if (callback) callback(); + }; + } + // 科目グループ定義(仕訳入力と同期) const ACCOUNT_GROUPS = [ // 資産の部 @@ -438,6 +477,14 @@ + + + `; @@ -464,6 +511,11 @@ const creditVal = Number(line.credit) || 0; numberInputs[0].value = debitVal > 0 ? String(debitVal) : ""; numberInputs[1].value = creditVal > 0 ? String(creditVal) : ""; + // 税率を設定 + if (line.tax_rate != null) { + const sel = tr.querySelector(".tax-rate-select"); + sel.value = String(line.tax_rate); + } // 摘要(行)を設定 const memoInput = tr.querySelector(".line-memo-input"); if (memoInput) memoInput.value = line.line_description || ""; @@ -556,11 +608,17 @@ const credit = Number(numberInputs[1]?.value || 0); const lineDesc = tr.querySelector(".line-memo-input")?.value.trim() || undefined; + const taxRateRaw = tr.querySelector(".tax-rate-select")?.value; + const taxRate = + taxRateRaw !== "" && taxRateRaw != null + ? Number(taxRateRaw) + : undefined; if (debit === 0 && credit === 0) return; lines.push({ account_id: accountId, debit, credit, + ...(taxRate !== undefined ? { tax_rate: taxRate } : {}), ...(lineDesc ? { line_description: lineDesc } : {}), }); d += debit; @@ -599,15 +657,17 @@ if (!res.ok) return showError(data.detail || "登録失敗"); // 登録成功 - 親窓口の検索を再実行してから画面を閉じる - alert(`登録しました(ID: ${data.journal_entry_id})`); - localStorage.removeItem("editSource"); - - // 親窓口(列表页)の searchJournals()を呼び出す - if (window.opener && window.opener.searchJournals) { - window.opener.searchJournals(); - } - - window.close(); + showAlert( + `登録しました(ID: ${data.journal_entry_id})`, + "success", + () => { + localStorage.removeItem("editSource"); + if (window.opener && window.opener.searchJournals) { + window.opener.searchJournals(); + } + window.close(); + }, + ); } catch { showError("通信エラー"); } @@ -629,5 +689,57 @@ window.close(); } + +
+
+
+
+ +
+
diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index aa65b4e..391bc80 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -1,4 +1,4 @@ - + @@ -129,7 +129,7 @@ } @media print { @page { - margin: 20mm 20mm 25mm 20mm; + margin: 5mm 15mm 20mm 15mm; @bottom-center { content: "ページ " counter(page); font-size: 0.75em; @@ -869,6 +869,40 @@ +
+ diff --git a/frontend/journal-view.html b/frontend/journal-view.html index 7d32046..af67035 100644 --- a/frontend/journal-view.html +++ b/frontend/journal-view.html @@ -48,6 +48,7 @@ 科目名 借方 貸方 + 税率 摘要(行) @@ -124,6 +125,7 @@ ${ line.credit > 0 ? line.credit.toLocaleString() : "" } + ${line.tax_rate != null ? line.tax_rate + "%" : ""} ${line.line_description || ""} `; tbody.appendChild(tr); @@ -140,6 +142,7 @@ ${totalDebit.toLocaleString()} ${totalCredit.toLocaleString()} + `; tbody.appendChild(totalRow); } diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 1bd24d0..e7e8b61 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -195,14 +195,22 @@
+
+
+

計算オプション

+
+ + +
+
+ + + `; + }) + .join(""); return ` -
-
-
- ${p.payroll_year}年${ - p.payroll_month - }月 - ${getStatusLabel(p.status)} -
-
- ${empLabel} | 支給日: ${p.payment_date} -
-
- 差引支給額: ¥${Number( - p.net_payment, - ).toLocaleString()} - (総支給: ¥${Number( - p.total_payment, - ).toLocaleString()} - 控除: ¥${Number( - p.total_deduction, - ).toLocaleString()}) -
-
- -
- `; +
+
+ ${Number(y)}年 ${Number(m)}月 +
+ ${items} +
+ `; }) .join(""); - document.getElementById("payrollList").innerHTML = - html || "

給与データがありません

"; + document.getElementById("payrollList").innerHTML = html; } catch (error) { alert("給与一覧の取得に失敗しました"); console.error(error); @@ -927,6 +1049,11 @@ // ensure payroll_year/month are numbers data.payroll_year = valid.year; data.payroll_month = valid.month; + // 計算オプションフラグ + data.calc_income_tax = + document.getElementById("calcIncomeTax")?.checked ?? true; + data.calc_social_insurance = + document.getElementById("calcSocialInsurance")?.checked ?? true; try { const response = await fetch( @@ -942,6 +1069,7 @@ const result = await response.json(); alert("給与計算が完了しました"); closeCalculateForm(); + await loadPayrolls(); viewPayroll(result.payroll_id); } else { const error = await response.json(); @@ -1188,6 +1316,20 @@ +
+

計算オプション

+
+ + +
+
+ @@ -1224,11 +1366,20 @@ ); if (response.ok) { + // チェックボックスの値をフォーム削除前に取得 + const calcIncomeTax = + document.getElementById("editCalcIncomeTax")?.checked ?? true; + const calcSocialInsurance = + document.getElementById("editCalcSocialInsurance")?.checked ?? + true; alert("給与データを更新しました"); form.closest("div").remove(); // 再計算 - await recalculatePayroll(payrollId); + await recalculatePayroll(payrollId, { + calc_income_tax: calcIncomeTax, + calc_social_insurance: calcSocialInsurance, + }); } else { const error = await response.json(); alert("更新に失敗しました: " + error.detail); @@ -1266,14 +1417,17 @@ } } - async function recalculatePayroll(payrollId) { + async function recalculatePayroll(payrollId, calcOptions = null) { if (!confirm("給与を再計算しますか?")) return; try { + const body = calcOptions || {}; const response = await fetch( `${API_BASE}/payroll/calculation/${payrollId}/recalculate`, { method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), }, ); @@ -1320,11 +1474,12 @@ } } - // 初期化: 月ピッカーにセットして一覧を読み込む + // 初期化: 年フィルタに現在年をセットして一覧を読み込む const now = new Date(); const ym = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); - document.getElementById("filterYearMonth").value = ym; + const filterYearEl = document.getElementById("filterYear"); + if (filterYearEl) filterYearEl.value = now.getFullYear(); document.getElementById("filterBonusYearMonth").value = ym; loadPayrolls(); @@ -1359,8 +1514,11 @@ now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); - document.getElementById("voucherStartYearMonth").value = ym; - document.getElementById("voucherEndYearMonth").value = ""; + document.getElementById("voucherStartYear").value = now.getFullYear(); + document.getElementById("voucherStartMonth").value = + now.getMonth() + 1; + document.getElementById("voucherEndYear").value = ""; + document.getElementById("voucherEndMonth").value = ""; } } @@ -1732,8 +1890,13 @@ async function printVouchers() { // 検証 - const startYM = document.getElementById("voucherStartYearMonth").value; - if (!startYM) { + const startYear = Number( + document.getElementById("voucherStartYear").value, + ); + const startMonth = Number( + document.getElementById("voucherStartMonth").value, + ); + if (!startYear || !startMonth) { alert("開始年月を選択してください"); return; } @@ -1745,11 +1908,10 @@ } // 年月を解析 - const [startYear, startMonth] = startYM.split("-").map(Number); - const endYM = document.getElementById("voucherEndYearMonth").value; - const endParts = endYM ? endYM.split("-").map(Number) : null; - const endYear = endParts ? endParts[0] : null; - const endMonth = endParts ? endParts[1] : null; + const endYearVal = document.getElementById("voucherEndYear").value; + const endMonthVal = document.getElementById("voucherEndMonth").value; + const endYear = endYearVal ? Number(endYearVal) : null; + const endMonth = endMonthVal ? Number(endMonthVal) : null; const voucherType = document.querySelector( "input[name='voucherType']:checked", @@ -1900,6 +2062,7 @@
${salary.payroll_year}年${String(salary.payroll_month).padStart(2, "0")}月 給与明細
従業員: ${emp.employee_code} ${emp.employee_name}
支給日: ${paymentDate}
+ ${emp.employment_type ? `
雇用形態:${emp.employment_type}
` : ""}
勤怠情報
@@ -2013,6 +2176,7 @@
${bonus.bonus_year}年${String(bonus.bonus_month).padStart(2, "0")}月 賞与明細${bonus.bonus_type ? ` (${bonus.bonus_type})` : ""}
従業員: ${emp.employee_code} ${emp.employee_name}
支給日: ${bonusPaymentDate}
+ ${emp.employment_type ? `
雇用形態:${emp.employment_type}
` : ""}
支給項目
@@ -2103,8 +2267,13 @@ async function exportVouchersCSV() { // 検証 - const startYM = document.getElementById("voucherStartYearMonth").value; - if (!startYM) { + const startYear = Number( + document.getElementById("voucherStartYear").value, + ); + const startMonth = Number( + document.getElementById("voucherStartMonth").value, + ); + if (!startYear || !startMonth) { alert("開始年月を選択してください"); return; } @@ -2115,11 +2284,10 @@ return; } - const [startYear, startMonth] = startYM.split("-").map(Number); - const endYM = document.getElementById("voucherEndYearMonth").value; - const endParts = endYM ? endYM.split("-").map(Number) : null; - const endYear = endParts ? endParts[0] : null; - const endMonth = endParts ? endParts[1] : null; + const endYearVal = document.getElementById("voucherEndYear").value; + const endMonthVal = document.getElementById("voucherEndMonth").value; + const endYear = endYearVal ? Number(endYearVal) : null; + const endMonth = endMonthVal ? Number(endMonthVal) : null; const voucherType = document.querySelector( "input[name='voucherType']:checked", ).value; diff --git a/frontend/payroll-salary-settings.html b/frontend/payroll-salary-settings.html index a41d94b..ab359f4 100644 --- a/frontend/payroll-salary-settings.html +++ b/frontend/payroll-salary-settings.html @@ -326,10 +326,9 @@ : "✗ 非対象" } - `, @@ -390,6 +389,37 @@ } } + 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"; @@ -416,8 +446,18 @@ 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 = { - employee_id: parseInt(formData.get("employee_id")), base_salary: parseFloat(formData.get("base_salary")) || 0, employment_type: formData.get("employment_type"), payment_type: formData.get("payment_type"), @@ -429,6 +469,11 @@ 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")); } @@ -465,11 +510,24 @@ hideAddForm(); loadAllSalarySettings(); } else { - const error = await response.json(); - alert("保存に失敗しました: " + error.detail); + 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("保存に失敗しました"); + alert("保存に失敗しました: " + (error.message || error)); console.error(error); } }
${s.valid_from} ~ ${s.valid_to || "現在"} - + + +