2679 lines
106 KiB
HTML
2679 lines
106 KiB
HTML
<!doctype html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta
|
||
http-equiv="Cache-Control"
|
||
content="no-cache, no-store, must-revalidate"
|
||
/>
|
||
<meta http-equiv="Pragma" content="no-cache" />
|
||
<meta http-equiv="Expires" content="0" />
|
||
<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>
|
||
.container {
|
||
max-width: 1440px;
|
||
margin: 0 auto;
|
||
padding: 0 20px;
|
||
}
|
||
.filters {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #f9f9f9;
|
||
border-radius: 5px;
|
||
}
|
||
.filters label {
|
||
margin-right: 15px;
|
||
}
|
||
.payroll-list {
|
||
margin-top: 20px;
|
||
}
|
||
.payroll-item {
|
||
padding: 15px;
|
||
border: 1px solid #ddd;
|
||
margin-bottom: 10px;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
}
|
||
.payroll-item:hover {
|
||
background: #f9f9f9;
|
||
}
|
||
.status-badge {
|
||
padding: 4px 8px;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
font-weight: bold;
|
||
}
|
||
.status-draft {
|
||
background: #ffc107;
|
||
color: #000;
|
||
}
|
||
.status-calculated {
|
||
background: #17a2b8;
|
||
color: #fff;
|
||
}
|
||
.status-approved {
|
||
background: #28a745;
|
||
color: #fff;
|
||
}
|
||
.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;
|
||
}
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 15px;
|
||
}
|
||
.btn {
|
||
padding: 10px 20px;
|
||
margin-right: 10px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
.btn-primary {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
.btn-success {
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
.btn-secondary {
|
||
background: #6c757d;
|
||
color: white;
|
||
}
|
||
.payroll-detail {
|
||
margin-top: 20px;
|
||
}
|
||
/* 明細を中央に寄せ、横幅を狭める(読みやすくするため) */
|
||
.payroll-detail {
|
||
max-width: 760px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
padding-left: 8px;
|
||
padding-right: 8px;
|
||
}
|
||
.detail-section {
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #f9f9f9;
|
||
border-radius: 5px;
|
||
}
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
.detail-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.total-row {
|
||
font-weight: bold;
|
||
font-size: 1.2em;
|
||
color: #007bff;
|
||
}
|
||
.calc-note {
|
||
padding: 3px 10px 6px 14px;
|
||
font-size: 11px;
|
||
color: #555;
|
||
background: #f0f4f8;
|
||
border-left: 3px solid #b0c4de;
|
||
margin: -1px 0 4px 0;
|
||
line-height: 1.8;
|
||
}
|
||
.back-link {
|
||
display: inline-block;
|
||
margin-bottom: 20px;
|
||
color: #007bff;
|
||
text-decoration: none;
|
||
font-size: 14px;
|
||
}
|
||
.back-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
/* タブスタイル */
|
||
.nav-tabs-calc {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
border-bottom: 2px solid #ddd;
|
||
}
|
||
.nav-tab-calc {
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
border: none;
|
||
background: #f0f0f0;
|
||
border-radius: 5px 5px 0 0;
|
||
}
|
||
.nav-tab-calc.active {
|
||
background: #007bff;
|
||
color: white;
|
||
}
|
||
.tab-content-calc {
|
||
display: none;
|
||
}
|
||
.tab-content-calc.active {
|
||
display: block;
|
||
}
|
||
/* 左右分割レイアウト */
|
||
.split-layout {
|
||
display: flex;
|
||
gap: 0;
|
||
align-items: flex-start;
|
||
}
|
||
.split-left {
|
||
flex: 0 0 420px;
|
||
min-width: 320px;
|
||
max-width: 420px;
|
||
max-height: calc(100vh - 160px);
|
||
overflow-y: auto;
|
||
position: sticky;
|
||
top: 20px;
|
||
}
|
||
.split-right {
|
||
display: none;
|
||
flex: 1;
|
||
min-width: 0;
|
||
border-left: 2px solid #e0e8f0;
|
||
padding-left: 24px;
|
||
max-height: calc(100vh - 160px);
|
||
overflow-y: auto;
|
||
position: sticky;
|
||
top: 20px;
|
||
}
|
||
.split-right .payroll-detail {
|
||
max-width: none;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
margin-top: 0;
|
||
}
|
||
</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>
|
||
|
||
<div class="nav-tabs-calc">
|
||
<button class="nav-tab-calc active" onclick="switchTab('salary')">
|
||
給与計算
|
||
</button>
|
||
<button class="nav-tab-calc" onclick="switchTab('bonus')">
|
||
賞与計算
|
||
</button>
|
||
<button class="nav-tab-calc" onclick="switchTab('voucher')">
|
||
📄 账票出力
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 給与計算タブ -->
|
||
<div id="tab-salary" class="tab-content-calc active">
|
||
<div class="split-layout">
|
||
<div class="split-left">
|
||
<div
|
||
class="filters"
|
||
style="display: flex; flex-direction: column; gap: 8px"
|
||
>
|
||
<label style="display: flex; align-items: center; gap: 8px">
|
||
<span style="white-space: nowrap; min-width: 60px"
|
||
>対象年:</span
|
||
>
|
||
<input
|
||
type="number"
|
||
id="filterYear"
|
||
style="width: 90px"
|
||
value="2026"
|
||
min="2000"
|
||
max="2099"
|
||
/>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 8px">
|
||
<span style="white-space: nowrap; min-width: 60px"
|
||
>従業員:</span
|
||
>
|
||
<select id="filterEmployee" style="flex: 1">
|
||
<option value="">全員</option>
|
||
</select>
|
||
</label>
|
||
<div style="display: flex; gap: 8px">
|
||
<button class="btn btn-primary" onclick="loadPayrolls()">
|
||
検索
|
||
</button>
|
||
<button class="btn btn-success" onclick="showCalculateForm()">
|
||
新規計算
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="payrollList" class="payroll-list"></div>
|
||
</div>
|
||
<!-- /split-left -->
|
||
<div class="split-right" id="splitRightSalary">
|
||
<div
|
||
id="payrollDetail"
|
||
class="payroll-detail"
|
||
style="display: none"
|
||
></div>
|
||
</div>
|
||
<!-- /split-right -->
|
||
</div>
|
||
<!-- /split-layout -->
|
||
|
||
<!-- 給与計算フォーム(モーダル風) -->
|
||
<div
|
||
id="calculateModal"
|
||
style="
|
||
display: none;
|
||
position: fixed;
|
||
top: 50px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 800px;
|
||
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 id="calculateForm" onsubmit="calculatePayroll(event)">
|
||
<div class="form-group">
|
||
<label>従業員 *</label>
|
||
<select name="employee_id" id="employeeSelect" required>
|
||
<option value="">選択してください</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>対象年 *</label>
|
||
<input
|
||
type="text"
|
||
name="payroll_year"
|
||
inputmode="numeric"
|
||
pattern="\d{4}"
|
||
maxlength="4"
|
||
oninput="sanitizeDigits(this, 4)"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>対象月 *</label>
|
||
<select name="payroll_month" required>
|
||
<option value="">選択してください</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>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>支給日 *</label>
|
||
<input
|
||
type="date"
|
||
name="payment_date"
|
||
required
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>勤怠情報</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>出勤日数</label>
|
||
<input
|
||
type="number"
|
||
name="working_days"
|
||
step="0.5"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>勤務時間</label>
|
||
<input
|
||
type="number"
|
||
name="working_hours"
|
||
step="0.5"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>残業時間</label>
|
||
<input
|
||
type="number"
|
||
name="overtime_hours"
|
||
step="0.5"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>休日出勤時間</label>
|
||
<input
|
||
type="number"
|
||
name="holiday_hours"
|
||
step="0.5"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>遅刻時間</label>
|
||
<input type="number" name="late_hours" step="0.5" value="0" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>欠勤日数</label>
|
||
<input
|
||
type="number"
|
||
name="absent_days"
|
||
step="0.5"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>その他</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>その他手当</label>
|
||
<input
|
||
type="number"
|
||
name="other_allowance"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>住民税</label>
|
||
<input
|
||
type="number"
|
||
name="resident_tax"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他控除</label>
|
||
<input
|
||
type="number"
|
||
name="other_deduction"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>計算オプション</h3>
|
||
<div style="display: flex; gap: 30px; padding: 8px 0">
|
||
<label
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
<input type="checkbox" id="calcIncomeTax" checked />
|
||
所得税を計算する
|
||
</label>
|
||
<label
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
<input type="checkbox" id="calcSocialInsurance" checked />
|
||
社会保険を計算する
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">計算実行</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="closeCalculateForm()"
|
||
>
|
||
キャンセル
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 賞与計算タブ -->
|
||
<div id="tab-bonus" class="tab-content-calc">
|
||
<div class="split-layout">
|
||
<div class="split-left">
|
||
<div class="filters">
|
||
<label>
|
||
対象年:
|
||
<input
|
||
type="number"
|
||
id="filterBonusYear"
|
||
style="width: 90px"
|
||
value="2026"
|
||
min="2000"
|
||
max="2099"
|
||
/>
|
||
</label>
|
||
<button class="btn btn-primary" onclick="loadBonus()">
|
||
検索
|
||
</button>
|
||
<button class="btn btn-success" onclick="showBonusForm()">
|
||
新規計算
|
||
</button>
|
||
</div>
|
||
|
||
<div id="bonusList" class="payroll-list"></div>
|
||
</div>
|
||
<!-- /split-left -->
|
||
<div class="split-right" id="splitRightBonus">
|
||
<div
|
||
id="bonusDetail"
|
||
class="payroll-detail"
|
||
style="display: none"
|
||
></div>
|
||
</div>
|
||
<!-- /split-right -->
|
||
</div>
|
||
<!-- /split-layout -->
|
||
|
||
<!-- 賞与計算フォーム -->
|
||
<div
|
||
id="bonusModal"
|
||
style="
|
||
display: none;
|
||
position: fixed;
|
||
top: 50px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 800px;
|
||
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 id="bonusForm" onsubmit="calculateBonus(event)">
|
||
<div class="form-group">
|
||
<label>従業員 *</label>
|
||
<select name="employee_id" id="bonusEmployeeSelect" required>
|
||
<option value="">選択してください</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>対象年 *</label>
|
||
<input
|
||
type="text"
|
||
name="bonus_year"
|
||
inputmode="numeric"
|
||
pattern="\d{4}"
|
||
maxlength="4"
|
||
oninput="sanitizeDigits(this, 4)"
|
||
required
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>対象月 *</label>
|
||
<select name="bonus_month" required>
|
||
<option value="">選択してください</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>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>賞与種別 *</label>
|
||
<select name="bonus_type" required>
|
||
<option value="">選択してください</option>
|
||
<option value="夏季賞与">夏季賞与</option>
|
||
<option value="冬季賞与">冬季賞与</option>
|
||
<option value="決算賞与">決算賞与</option>
|
||
<option value="その他">その他</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>支給日 *</label>
|
||
<input
|
||
type="date"
|
||
name="payment_date"
|
||
required
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>基本賞与額</label>
|
||
<input type="number" name="base_bonus" step="0.01" value="0" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>業績賞与</label>
|
||
<input
|
||
type="number"
|
||
name="performance_bonus"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>その他賞与</label>
|
||
<input type="number" name="other_bonus" step="0.01" value="0" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>住民税</label>
|
||
<input
|
||
type="number"
|
||
name="resident_tax"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他控除</label>
|
||
<input
|
||
type="number"
|
||
name="other_deduction"
|
||
step="0.01"
|
||
value="0"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">計算実行</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-secondary"
|
||
onclick="closeBonusForm()"
|
||
>
|
||
キャンセル
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 账票出力タブ -->
|
||
<div id="tab-voucher" class="tab-content-calc">
|
||
<div class="filters">
|
||
<h3>出力条件の設定</h3>
|
||
</div>
|
||
|
||
<!-- 時間範囲選択 -->
|
||
<div class="detail-section" style="background: #f9f9f9">
|
||
<h3>期間選択</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>開始年月 *</label>
|
||
<div style="display: flex; gap: 6px; align-items: center">
|
||
<input
|
||
type="text"
|
||
id="voucherStartYear"
|
||
style="width: 90px"
|
||
placeholder="年"
|
||
maxlength="4"
|
||
pattern="[0-9]{4}"
|
||
inputmode="numeric"
|
||
required
|
||
/>
|
||
<span style="color: #666">年</span>
|
||
<select id="voucherStartMonth" required>
|
||
<option value="">月</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>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
<span style="color: #666">月</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>終了年月</label>
|
||
<div style="display: flex; gap: 6px; align-items: center">
|
||
<input
|
||
type="text"
|
||
id="voucherEndYear"
|
||
style="width: 90px"
|
||
placeholder="年"
|
||
maxlength="4"
|
||
pattern="[0-9]{4}"
|
||
inputmode="numeric"
|
||
/>
|
||
<span style="color: #666">年</span>
|
||
<select id="voucherEndMonth">
|
||
<option value="">月</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>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
</select>
|
||
<span style="color: #666">月</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="color: #999; font-size: 12px">
|
||
※終了年月を空にすると開始月のみを対象にします
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 従業員選択 -->
|
||
<div class="detail-section" style="background: #f9f9f9">
|
||
<h3>従業員選択</h3>
|
||
<div style="margin-bottom: 10px">
|
||
<button
|
||
class="btn btn-primary"
|
||
style="padding: 8px 15px"
|
||
onclick="selectAllVoucherEmployees()"
|
||
>
|
||
全選択
|
||
</button>
|
||
<button
|
||
class="btn btn-secondary"
|
||
style="padding: 8px 15px"
|
||
onclick="clearAllVoucherEmployees()"
|
||
>
|
||
全解除
|
||
</button>
|
||
<span
|
||
id="voucherEmployeeCount"
|
||
style="margin-left: 20px; font-weight: bold"
|
||
>選択数: 0</span
|
||
>
|
||
</div>
|
||
<div
|
||
id="voucherEmployeeList"
|
||
style="
|
||
border: 1px solid #ddd;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
background: white;
|
||
"
|
||
>
|
||
<p style="color: #999">従業員を読み込み中...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 支払類型選択 -->
|
||
<div class="detail-section" style="background: #f9f9f9">
|
||
<h3>支払タイプ</h3>
|
||
<div style="display: flex; gap: 30px">
|
||
<label style="font-weight: normal">
|
||
<input type="radio" name="voucherType" value="all" checked />
|
||
<span>給与・賞与両方</span>
|
||
</label>
|
||
<label style="font-weight: normal">
|
||
<input type="radio" name="voucherType" value="salary" />
|
||
<span>給与のみ</span>
|
||
</label>
|
||
<label style="font-weight: normal">
|
||
<input type="radio" name="voucherType" value="bonus" />
|
||
<span>賞与のみ</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作ボタン -->
|
||
<div style="margin: 20px 0; display: flex; gap: 10px">
|
||
<button
|
||
class="btn btn-success"
|
||
id="printVouchersBtn"
|
||
onclick="printVouchers()"
|
||
style="background: #007bff"
|
||
>
|
||
📄 印刷
|
||
</button>
|
||
<button
|
||
class="btn btn-success"
|
||
onclick="exportVouchersCSV()"
|
||
style="background: #28a745"
|
||
>
|
||
CSV出力
|
||
</button>
|
||
<!-- PDF出力は後の実装用 -->
|
||
<!-- <button class="btn btn-info" onclick="exportVouchersPDF()">PDF出力</button> -->
|
||
</div>
|
||
|
||
<!-- 結果表示エリア -->
|
||
<div id="voucherResultArea" style="display: none; margin-top: 30px">
|
||
<div
|
||
id="voucherResultSummary"
|
||
style="padding: 15px; border-radius: 5px; margin-bottom: 20px"
|
||
>
|
||
<!-- 結果摘要がここに表示される -->
|
||
</div>
|
||
|
||
<div
|
||
id="voucherResultData"
|
||
style="
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
padding: 20px;
|
||
"
|
||
>
|
||
<!-- 詳細データがここに表示される -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 無データアラートモーダル -->
|
||
<div
|
||
id="noDataModal"
|
||
style="
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 2000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
"
|
||
>
|
||
<div
|
||
style="
|
||
background: white;
|
||
padding: 30px;
|
||
border-radius: 10px;
|
||
max-width: 500px;
|
||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||
"
|
||
>
|
||
<h2 style="color: #d9534f; margin-top: 0">⚠️ データがありません</h2>
|
||
<p
|
||
id="noDataMessage"
|
||
style="font-size: 16px; line-height: 1.6; color: #333"
|
||
>
|
||
<!-- メッセージがここに表示される -->
|
||
</p>
|
||
<div style="text-align: right; margin-top: 20px">
|
||
<button
|
||
class="btn btn-primary"
|
||
onclick="
|
||
document.getElementById('noDataModal').style.display = 'none'
|
||
"
|
||
>
|
||
OK
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Use relative API paths so frontend works when served via nginx on the NAS.
|
||
// The nginx service will proxy API requests to the backend container.
|
||
const API_BASE = "";
|
||
let employeeMap = {};
|
||
|
||
// sanitize input to digits only and limit length
|
||
function sanitizeDigits(el, maxLen) {
|
||
if (!el) return;
|
||
// remove non-digits
|
||
el.value = el.value.replace(/\D/g, "");
|
||
if (maxLen && el.value.length > maxLen)
|
||
el.value = el.value.slice(0, maxLen);
|
||
}
|
||
|
||
function validateYearMonth(yearStr, monthStr) {
|
||
const now = new Date();
|
||
const currentYear = now.getFullYear();
|
||
if (!/^[0-9]{4}$/.test(String(yearStr)))
|
||
return {
|
||
ok: false,
|
||
message: "対象年は4桁の数字で入力してください(例: 2026)",
|
||
};
|
||
const year = Number(yearStr);
|
||
if (year < 1900 || year > currentYear + 1)
|
||
return {
|
||
ok: false,
|
||
message: `対象年は1900〜${currentYear + 1}の範囲で入力してください`,
|
||
};
|
||
if (!/^[0-9]{1,2}$/.test(String(monthStr)))
|
||
return {
|
||
ok: false,
|
||
message: "対象月は数字のみで入力してください(1〜12)",
|
||
};
|
||
const month = Number(monthStr);
|
||
if (month < 1 || month > 12)
|
||
return {
|
||
ok: false,
|
||
message: "対象月は1〜12の範囲で入力してください",
|
||
};
|
||
return { ok: true, year, month };
|
||
}
|
||
|
||
async function loadEmployees() {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/employees/?is_active=true`,
|
||
);
|
||
const employees = await response.json();
|
||
|
||
// マップを作成して再利用できるようにする
|
||
employeeMap = {};
|
||
employees.forEach((emp) => {
|
||
employeeMap[emp.employee_id] = emp;
|
||
});
|
||
|
||
// 給与フォームと賞与フォーム両方のセレクトボックスを更新
|
||
const optionsHtml =
|
||
'<option value="">選択してください</option>' +
|
||
employees
|
||
.map(
|
||
(emp) =>
|
||
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
|
||
)
|
||
.join("");
|
||
|
||
const salarySelect = document.getElementById("employeeSelect");
|
||
if (salarySelect) salarySelect.innerHTML = optionsHtml;
|
||
|
||
const bonusSelect = document.getElementById("bonusEmployeeSelect");
|
||
if (bonusSelect) bonusSelect.innerHTML = optionsHtml;
|
||
|
||
// 検索用フィルタのドロップダウンも更新
|
||
const filterSelect = document.getElementById("filterEmployee");
|
||
if (filterSelect) {
|
||
const currentVal = filterSelect.value;
|
||
filterSelect.innerHTML =
|
||
'<option value="">全員</option>' +
|
||
employees
|
||
.map(
|
||
(emp) =>
|
||
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
|
||
)
|
||
.join("");
|
||
filterSelect.value = currentVal;
|
||
}
|
||
|
||
return employees;
|
||
}
|
||
|
||
async function loadPayrolls() {
|
||
// 前回の給与明細をクリア
|
||
const detailDiv = document.getElementById("payrollDetail");
|
||
if (detailDiv) {
|
||
detailDiv.style.display = "none";
|
||
detailDiv.innerHTML = "";
|
||
}
|
||
|
||
const year =
|
||
Number(document.getElementById("filterYear")?.value) || null;
|
||
const employeeId =
|
||
document.getElementById("filterEmployee")?.value || null;
|
||
|
||
const params = new URLSearchParams();
|
||
if (year) params.append("payroll_year", year);
|
||
if (employeeId) params.append("employee_id", employeeId);
|
||
|
||
// employeeMap が空なら読み込む
|
||
if (!employeeMap || Object.keys(employeeMap).length === 0) {
|
||
await loadEmployees();
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/?${params}`,
|
||
);
|
||
const payrolls = await response.json();
|
||
|
||
if (!payrolls || payrolls.length === 0) {
|
||
document.getElementById("payrollList").innerHTML =
|
||
"<p>給与データがありません</p>";
|
||
return;
|
||
}
|
||
|
||
// 月別にグループ化
|
||
const byMonth = {};
|
||
payrolls.forEach((p) => {
|
||
const key = `${p.payroll_year}-${String(p.payroll_month).padStart(2, "0")}`;
|
||
if (!byMonth[key]) byMonth[key] = [];
|
||
byMonth[key].push(p);
|
||
});
|
||
|
||
// 月順にソート
|
||
const sortedMonths = Object.keys(byMonth).sort();
|
||
|
||
const html = sortedMonths
|
||
.map((monthKey) => {
|
||
const [y, m] = monthKey.split("-");
|
||
const items = byMonth[monthKey]
|
||
.map((p) => {
|
||
const emp = employeeMap[p.employee_id];
|
||
const empLabel = emp
|
||
? `${emp.employee_code} - ${emp.name}`
|
||
: `従業員ID: ${p.employee_id}`;
|
||
return `
|
||
<div class="payroll-item" style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||
<div style="flex:1; min-width:0; cursor:pointer;" onclick="viewPayroll(${p.payroll_id})">
|
||
<div>
|
||
<strong>${empLabel}</strong>
|
||
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
|
||
</div>
|
||
<div style="font-size:12px; color:#666; margin-top:2px; white-space:nowrap;">支給日: ${p.payment_date}</div>
|
||
<div style="margin-top:4px; white-space:nowrap;"><strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong></div>
|
||
<div style="color:#666; font-size:13px; white-space:nowrap;">(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})</div>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deletePayroll(${p.payroll_id}); event.stopPropagation();" style="margin-left:10px; white-space:nowrap; background-color:#dc3545; color:#fff; border:none; flex-shrink:0;">
|
||
削除
|
||
</button>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
return `
|
||
<div style="margin-bottom:20px;">
|
||
<div style="background:#e8f0fe; border-left:4px solid #007bff; padding:8px 14px; font-weight:bold; font-size:15px; margin-bottom:6px;">
|
||
${Number(y)}年 ${Number(m)}月
|
||
</div>
|
||
${items}
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
document.getElementById("payrollList").innerHTML = html;
|
||
} catch (error) {
|
||
alert("給与一覧の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function getStatusLabel(status) {
|
||
const labels = {
|
||
draft: "下書き",
|
||
calculated: "計算済み",
|
||
approved: "承認済み",
|
||
paid: "支払済み",
|
||
};
|
||
return labels[status] || status;
|
||
}
|
||
|
||
function showCalculateForm() {
|
||
document.getElementById("calculateModal").style.display = "block";
|
||
loadEmployees();
|
||
|
||
// デフォルト値設定
|
||
const now = new Date();
|
||
document.querySelector('[name="payroll_year"]').value =
|
||
now.getFullYear();
|
||
document.querySelector('[name="payroll_month"]').value =
|
||
now.getMonth() + 1;
|
||
}
|
||
|
||
function closeCalculateForm() {
|
||
document.getElementById("calculateModal").style.display = "none";
|
||
document.getElementById("calculateForm").reset();
|
||
}
|
||
|
||
async function calculatePayroll(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
// validate year/month
|
||
const yearStr = formData.get("payroll_year");
|
||
const monthStr = formData.get("payroll_month");
|
||
const valid = validateYearMonth(yearStr, monthStr);
|
||
if (!valid.ok) {
|
||
alert(valid.message);
|
||
return;
|
||
}
|
||
|
||
const data = {};
|
||
formData.forEach((value, key) => {
|
||
// convert numeric-like fields to Number, keep others as-is
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
// 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(
|
||
`${API_BASE}/payroll/calculation/calculate`,
|
||
{
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
alert("給与計算が完了しました");
|
||
closeCalculateForm();
|
||
await loadPayrolls();
|
||
viewPayroll(result.payroll_id);
|
||
} else {
|
||
const error = await response.json();
|
||
const errorMessage =
|
||
typeof error.detail === "string"
|
||
? error.detail
|
||
: JSON.stringify(error.detail || error);
|
||
alert("計算に失敗しました:\n\n" + errorMessage);
|
||
}
|
||
} catch (error) {
|
||
alert("計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function viewPayroll(payrollId) {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||
);
|
||
const payroll = await response.json();
|
||
|
||
// 従業員情報と扶養家族情報を取得
|
||
const empResponse = await fetch(
|
||
`${API_BASE}/payroll/employees/${payroll.employee_id}`,
|
||
);
|
||
const employee = await empResponse.json();
|
||
|
||
// 源泉税控除対象人数を計算(16歳以上19歳未満の一般扶養親族 + 19歳以上23歳未満の特定扶養親族)
|
||
const dependentCount = employee.dependents
|
||
? employee.dependents.filter((d) => {
|
||
// 有効期間チェック
|
||
if (d.valid_to && new Date(d.valid_to) < new Date())
|
||
return false;
|
||
|
||
// 生年月日から年齢を計算(その年の12月31日時点)
|
||
if (!d.birth_date) return false;
|
||
const birthDate = new Date(d.birth_date);
|
||
const targetYear = payroll.payroll_year;
|
||
const yearEnd = new Date(targetYear, 11, 31); // 12月31日
|
||
let age = yearEnd.getFullYear() - birthDate.getFullYear();
|
||
const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
|
||
if (
|
||
monthDiff < 0 ||
|
||
(monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())
|
||
) {
|
||
age--;
|
||
}
|
||
|
||
// 16歳以上が源泉税控除対象(16-18歳は一般扶養、19-22歳は特定扶養)
|
||
return age >= 16;
|
||
}).length
|
||
: 0;
|
||
|
||
// 計算明細表示用の変数
|
||
const _health = Number(payroll.health_insurance || 0);
|
||
const _care = Number(payroll.care_insurance || 0);
|
||
const _pension = Number(payroll.pension_insurance || 0);
|
||
const _emp = Number(payroll.employment_insurance || 0);
|
||
const _child = Number(payroll.child_support || 0);
|
||
const _totalPay = Number(payroll.total_payment || 0);
|
||
// 標準報酬月額を厚生年金保険料(個人負担率 9.150%)から逆算
|
||
const _stdRemun = _pension > 0 ? Math.round(_pension / 0.0915) : 0;
|
||
const _stdFmt =
|
||
_stdRemun > 0 ? "¥" + _stdRemun.toLocaleString() : "-";
|
||
const _hRateFull =
|
||
_stdRemun > 0
|
||
? (((_health * 2) / _stdRemun) * 100).toFixed(2)
|
||
: "-";
|
||
const _hRateHalf =
|
||
_stdRemun > 0 ? ((_health / _stdRemun) * 100).toFixed(2) : "-";
|
||
const _cRateFull =
|
||
_stdRemun > 0 && _care > 0
|
||
? (((_care * 2) / _stdRemun) * 100).toFixed(2)
|
||
: null;
|
||
const _cRateHalf =
|
||
_stdRemun > 0 && _care > 0
|
||
? ((_care / _stdRemun) * 100).toFixed(2)
|
||
: null;
|
||
const _empRate =
|
||
_totalPay > 0 ? ((_emp / _totalPay) * 100).toFixed(3) : "-";
|
||
const _csRateFull =
|
||
_stdRemun > 0 && _child > 0
|
||
? (((_child * 2) / _stdRemun) * 100).toFixed(3)
|
||
: null;
|
||
const _csRateHalf =
|
||
_stdRemun > 0 && _child > 0
|
||
? ((_child / _stdRemun) * 100).toFixed(3)
|
||
: null;
|
||
const _socialSub = _health + _care + _pension + _emp + _child;
|
||
const _healthCareChild = _health + _care + _child;
|
||
const _taxable = _totalPay - _socialSub;
|
||
|
||
const html = `
|
||
<h2>${payroll.payroll_year}年${
|
||
payroll.payroll_month
|
||
}月 給与明細</h2>
|
||
<p><strong>従業員:</strong> ${employee.employee_code} - ${
|
||
employee.name
|
||
} | <strong>支給日:</strong> ${
|
||
payroll.payment_date
|
||
} | <strong>甲欄扶養親族人数(源泉税控除対象):</strong> ${dependentCount}人</p>
|
||
<p>ステータス: <span class="status-badge status-${
|
||
payroll.status
|
||
}">${getStatusLabel(payroll.status)}</span></p>
|
||
|
||
<div class="detail-section">
|
||
<h3>勤怠情報 ${
|
||
payroll.status === "calculated"
|
||
? '<button class="btn btn-primary" onclick="editPayroll(' +
|
||
payroll.payroll_id +
|
||
')" style="padding: 5px 10px; font-size: 12px; float: right;">編集</button>'
|
||
: ""
|
||
}</h3>
|
||
<div class="detail-row"><span>出勤日数:</span><span>${
|
||
payroll.working_days
|
||
}日</span></div>
|
||
<div class="detail-row"><span>勤務時間:</span><span>${
|
||
payroll.working_hours
|
||
}時間</span></div>
|
||
<div class="detail-row"><span>残業時間:</span><span>${
|
||
payroll.overtime_hours
|
||
}時間</span></div>
|
||
<div class="detail-row"><span>休日出勤:</span><span>${
|
||
payroll.holiday_hours
|
||
}時間</span></div>
|
||
<div class="detail-row"><span>欠勤日数:</span><span>${
|
||
payroll.absent_days
|
||
}日</span></div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>支給項目</h3>
|
||
<div class="detail-row"><span>基本給:</span><span>¥${Number(
|
||
payroll.base_salary,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>残業手当:</span><span>¥${Number(
|
||
payroll.overtime_pay,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>休日手当:</span><span>¥${Number(
|
||
payroll.holiday_pay,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>通勤手当:</span><span>¥${Number(
|
||
payroll.commute_allowance,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他手当:</span><span>¥${Number(
|
||
payroll.other_allowance,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
|
||
payroll.total_payment,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>健康保険:</span><span>¥${_health.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${_stdFmt}(標準報酬月額)× ${_hRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||
個人負担率:${_hRateHalf}% ※協会けんぽ東京
|
||
</div>
|
||
<div class="detail-row"><span>介護保険:</span><span>¥${_care.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${
|
||
_cRateFull
|
||
? `${_stdFmt}(標準報酬月額)× ${_cRateFull}%(保険料率・全体)÷ 2(折半)<br>個人負担率:${_cRateHalf}% ※40歳以上65歳未満が対象`
|
||
: "介護保険非該当(40歳未満または65歳以上)"
|
||
}
|
||
</div>
|
||
<div class="detail-row"><span>厚生年金:</span><span>¥${_pension.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${_stdFmt}(標準報酬月額)× 18.300%(保険料率・全体)÷ 2(折半)<br>
|
||
個人負担率:9.150%(固定)
|
||
</div>
|
||
<div class="detail-row"><span>雇用保険:</span><span>¥${_emp.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
総支給額 ¥${_totalPay.toLocaleString()} × ${_empRate}%(雇用保険料率・従業員負担分)<br>
|
||
※標準報酬でなく実際の総支給額に料率を乗じる
|
||
</div>
|
||
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_child.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${
|
||
_csRateFull
|
||
? `${_stdFmt}(標準報酬月額)× ${_csRateFull}%(拠出金率・全体)÷ 2(折半)<br>個人負担率:${_csRateHalf}% ※令和8年5月支払分(4月分)より控除開始`
|
||
: "令和8年5月支払分(4月分保険料)より控除開始 → 当月は対象外"
|
||
}
|
||
</div>
|
||
<div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7">
|
||
健康保険 ¥${_health.toLocaleString()} + 介護保険 ¥${_care.toLocaleString()} + 子ども・子育て支援金 ¥${_child.toLocaleString()} = <strong>¥${_healthCareChild.toLocaleString()}</strong><br>
|
||
¥${_healthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_pension.toLocaleString()}${' '}${_emp > 0 ? `+ 雇用保険 ¥${_emp.toLocaleString()}` : ''}= <strong>社会保険小計 ¥${_socialSub.toLocaleString()}</strong>
|
||
</div>
|
||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_socialSub.toLocaleString()}</span></div>
|
||
<div class="detail-row">
|
||
<span>所得税 <small style="color:#666;">(甲欄${dependentCount}人)</small>:</span>
|
||
<span>¥${Number(payroll.income_tax).toLocaleString()}</span>
|
||
</div>
|
||
<div class="calc-note">
|
||
課税対象額 = 総支給額 ¥${_totalPay.toLocaleString()} − 社会保険料計 ¥${_socialSub.toLocaleString()} = ¥${_taxable.toLocaleString()}<br>
|
||
→ 源泉徴収税額表(甲欄・扶養${dependentCount}人)を参照して計算
|
||
</div>
|
||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||
payroll.resident_tax,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
|
||
payroll.other_deduction,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
|
||
payroll.total_deduction,
|
||
).toLocaleString()}</span></div>
|
||
</div>
|
||
|
||
<div class="detail-section" style="background:#e7f3ff;">
|
||
<div class="detail-row total-row" style="font-size:1.5em;">
|
||
<span>差引支給額:</span>
|
||
<span>¥${Number(
|
||
payroll.net_payment,
|
||
).toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
|
||
${
|
||
payroll.status === "calculated"
|
||
? `
|
||
<button class="btn btn-success" onclick="approvePayroll(${payroll.payroll_id})">承認</button>
|
||
<button class="btn btn-primary" onclick="recalculatePayroll(${payroll.payroll_id})">再計算</button>
|
||
`
|
||
: ""
|
||
}
|
||
<button class="btn btn-secondary" onclick="document.getElementById('payrollDetail').style.display='none'; var sr=document.getElementById('splitRightSalary'); if(sr) sr.style.display='none';">閉じる</button>
|
||
`;
|
||
|
||
document.getElementById("payrollDetail").innerHTML = html;
|
||
const splitRightSalary = document.getElementById("splitRightSalary");
|
||
if (splitRightSalary) splitRightSalary.style.display = "block";
|
||
document.getElementById("payrollDetail").style.display = "block";
|
||
// モバイルでは明細パネルへ自動スクロール
|
||
if (window.innerWidth <= 767 && splitRightSalary) {
|
||
setTimeout(function() { splitRightSalary.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50);
|
||
}
|
||
} catch (error) {
|
||
alert("給与明細の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function editPayroll(payrollId) {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||
);
|
||
const payroll = await response.json();
|
||
|
||
// 編集フォームを表示
|
||
const html = `
|
||
<div style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: min(800px, 92vw); 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; box-sizing: border-box;">
|
||
<h2>給与データ編集</h2>
|
||
<form id="editPayrollForm" onsubmit="savePayrollEdit(event, ${payrollId})">
|
||
<div class="detail-section">
|
||
<h3>勤怠情報</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>出勤日数</label>
|
||
<input type="number" name="working_days" step="0.5" value="${payroll.working_days}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>勤務時間</label>
|
||
<input type="number" name="working_hours" step="0.5" value="${payroll.working_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>残業時間</label>
|
||
<input type="number" name="overtime_hours" step="0.5" value="${payroll.overtime_hours}" />
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>休日出勤時間</label>
|
||
<input type="number" name="holiday_hours" step="0.5" value="${payroll.holiday_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>遅刻時間</label>
|
||
<input type="number" name="late_hours" step="0.5" value="${payroll.late_hours}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>欠勤日数</label>
|
||
<input type="number" name="absent_days" step="0.5" value="${payroll.absent_days}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>その他</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>その他手当</label>
|
||
<input type="number" name="other_allowance" step="0.01" value="${payroll.other_allowance}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>住民税</label>
|
||
<input type="number" name="resident_tax" step="0.01" value="${payroll.resident_tax}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他控除</label>
|
||
<input type="number" name="other_deduction" step="0.01" value="${payroll.other_deduction}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>計算オプション</h3>
|
||
<div style="display:flex; gap:30px; padding:8px 0;">
|
||
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
|
||
<input type="checkbox" id="editCalcIncomeTax" checked />
|
||
所得税を計算する
|
||
</label>
|
||
<label style="display:flex; align-items:center; gap:6px; cursor:pointer;">
|
||
<input type="checkbox" id="editCalcSocialInsurance" checked />
|
||
社会保険を計算する
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn btn-primary">保存して再計算</button>
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('div').remove()">キャンセル</button>
|
||
</form>
|
||
</div>
|
||
`;
|
||
|
||
const editDiv = document.createElement("div");
|
||
editDiv.innerHTML = html;
|
||
document.body.appendChild(editDiv);
|
||
} catch (error) {
|
||
alert("給与データの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function savePayrollEdit(event, payrollId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
const data = {};
|
||
|
||
formData.forEach((value, key) => {
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||
{
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
},
|
||
);
|
||
|
||
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, {
|
||
calc_income_tax: calcIncomeTax,
|
||
calc_social_insurance: calcSocialInsurance,
|
||
});
|
||
} else {
|
||
const error = await response.json();
|
||
alert("更新に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("更新に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function approvePayroll(payrollId) {
|
||
if (!confirm("この給与を承認しますか?")) return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}/approve`,
|
||
{
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ approved_by: "admin" }),
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("承認しました");
|
||
viewPayroll(payrollId);
|
||
loadPayrolls();
|
||
} else {
|
||
const error = await response.json();
|
||
alert("承認に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("承認に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
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),
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("再計算が完了しました");
|
||
await loadPayrolls();
|
||
viewPayroll(payrollId);
|
||
} else {
|
||
const error = await response.json();
|
||
alert("再計算に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("再計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function deletePayroll(payrollId) {
|
||
if (
|
||
!confirm("この給与データを削除しますか?\nこの操作は取り消せません。")
|
||
)
|
||
return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/calculation/${payrollId}`,
|
||
{
|
||
method: "DELETE",
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("給与データを削除しました");
|
||
document.getElementById("payrollDetail").style.display = "none";
|
||
loadPayrolls();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(
|
||
"削除に失敗しました: " + JSON.stringify(error.detail || error),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
alert("削除に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// 初期化: 年フィルタに現在年をセットして一覧を読み込む
|
||
const now = new Date();
|
||
const ym =
|
||
now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
||
const filterYearEl = document.getElementById("filterYear");
|
||
if (filterYearEl) filterYearEl.value = now.getFullYear();
|
||
const filterBonusYearEl = document.getElementById("filterBonusYear");
|
||
if (filterBonusYearEl) filterBonusYearEl.value = now.getFullYear();
|
||
loadPayrolls();
|
||
|
||
// =====================================================
|
||
// タブ切り替え機能
|
||
// =====================================================
|
||
var currentTab = "salary";
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
|
||
// タブボタンの表示切り替え
|
||
document
|
||
.querySelectorAll(".nav-tab-calc")
|
||
.forEach((btn) => btn.classList.remove("active"));
|
||
event.target.classList.add("active");
|
||
|
||
// タブコンテンツの表示切り替え
|
||
document.getElementById("tab-salary").classList.remove("active");
|
||
document.getElementById("tab-bonus").classList.remove("active");
|
||
document.getElementById("tab-voucher").classList.remove("active");
|
||
document.getElementById("tab-" + tab).classList.add("active");
|
||
|
||
// 初回表示時に一覧を読み込む
|
||
if (tab === "bonus") {
|
||
loadBonus();
|
||
} else if (tab === "voucher") {
|
||
loadVoucherEmployees();
|
||
// 開始年月をデフォルト値として設定
|
||
const now = new Date();
|
||
const ym =
|
||
now.getFullYear() +
|
||
"-" +
|
||
String(now.getMonth() + 1).padStart(2, "0");
|
||
document.getElementById("voucherStartYear").value = now.getFullYear();
|
||
document.getElementById("voucherStartMonth").value =
|
||
now.getMonth() + 1;
|
||
document.getElementById("voucherEndYear").value = "";
|
||
document.getElementById("voucherEndMonth").value = "";
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 賞与計算関連
|
||
// =====================================================
|
||
async function loadBonus() {
|
||
const year = document.getElementById("filterBonusYear").value;
|
||
|
||
const params = new URLSearchParams();
|
||
if (year) params.append("bonus_year", Number(year));
|
||
|
||
// employeeMapが空なら読み込む
|
||
if (!employeeMap || Object.keys(employeeMap).length === 0) {
|
||
await loadEmployees();
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/?${params}`);
|
||
const bonuses = await response.json();
|
||
|
||
const html = bonuses
|
||
.map((b) => {
|
||
const emp = employeeMap[b.employee_id];
|
||
const empLabel = emp
|
||
? `${emp.employee_code} - ${emp.name}`
|
||
: `従業員: 未登録`;
|
||
|
||
return `
|
||
<div class="payroll-item" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div style="flex: 1; min-width: 0; cursor: pointer;" onclick="viewBonus(${b.bonus_id})">
|
||
<div>
|
||
<strong>${b.bonus_year}年${b.bonus_month}月</strong>
|
||
<span class="status-badge status-calculated">${b.bonus_type}</span>
|
||
</div>
|
||
<div style="font-size: 12px; color: #666; margin-top: 2px; white-space: nowrap;">
|
||
${empLabel} | 支給日: ${b.payment_date}
|
||
</div>
|
||
<div style="white-space: nowrap;"><strong>差引支給額: ¥${Number(b.net_bonus).toLocaleString()}</strong></div>
|
||
<div style="color: #666; font-size: 13px; white-space: nowrap;">(総支給: ¥${Number(b.total_bonus).toLocaleString()} - 控除: ¥${Number(b.total_deduction || 0).toLocaleString()})</div>
|
||
</div>
|
||
<button class="btn btn-danger btn-small" onclick="deleteBonus(${b.bonus_id}); event.stopPropagation();" style="margin-left: 10px; white-space: nowrap; flex-shrink: 0; background-color: #dc3545; color: #fff; border: none;">
|
||
削除
|
||
</button>
|
||
</div>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
document.getElementById("bonusList").innerHTML =
|
||
html || "<p>賞与データがありません</p>";
|
||
} catch (error) {
|
||
alert("賞与一覧の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
function showBonusForm() {
|
||
document.getElementById("bonusModal").style.display = "block";
|
||
loadEmployees();
|
||
|
||
// デフォルト値設定
|
||
const now = new Date();
|
||
document.querySelector("#bonusForm [name='bonus_year']").value =
|
||
now.getFullYear();
|
||
document.querySelector("#bonusForm [name='bonus_month']").value =
|
||
now.getMonth() + 1;
|
||
}
|
||
|
||
function closeBonusForm() {
|
||
document.getElementById("bonusModal").style.display = "none";
|
||
document.getElementById("bonusForm").reset();
|
||
}
|
||
|
||
async function calculateBonus(event) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
// validate year/month
|
||
const yearStr = formData.get("bonus_year");
|
||
const monthStr = formData.get("bonus_month");
|
||
const valid = validateYearMonth(yearStr, monthStr);
|
||
if (!valid.ok) {
|
||
alert(valid.message);
|
||
return;
|
||
}
|
||
|
||
const data = {};
|
||
formData.forEach((value, key) => {
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
data.bonus_year = valid.year;
|
||
data.bonus_month = valid.month;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/calculate`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
alert("賞与計算が完了しました");
|
||
closeBonusForm();
|
||
await loadBonus();
|
||
viewBonus(result.bonus_id);
|
||
} else {
|
||
const error = await response.json();
|
||
const errorMessage =
|
||
typeof error.detail === "string"
|
||
? error.detail
|
||
: JSON.stringify(error.detail || error);
|
||
alert("計算に失敗しました:\n\n" + errorMessage);
|
||
}
|
||
} catch (error) {
|
||
alert("計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function viewBonus(bonusId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`);
|
||
const bonus = await response.json();
|
||
|
||
// 従業員情報を取得
|
||
const empResponse = await fetch(
|
||
`${API_BASE}/payroll/employees/${bonus.employee_id}`,
|
||
);
|
||
const employee = await empResponse.json();
|
||
|
||
// 計算明細表示用の変数(賞与は総支給額に料率を乗じる)
|
||
const _bHealth = Number(bonus.health_insurance || 0);
|
||
|
||
// 源泉税控除対象人数を計算(給与と同じロジック)
|
||
const bDependentCount = employee.dependents
|
||
? employee.dependents.filter((d) => {
|
||
if (d.valid_to && new Date(d.valid_to) < new Date())
|
||
return false;
|
||
if (!d.birth_date) return false;
|
||
const birthDate = new Date(d.birth_date);
|
||
const targetYear = bonus.bonus_year;
|
||
const yearEnd = new Date(targetYear, 11, 31);
|
||
let age = yearEnd.getFullYear() - birthDate.getFullYear();
|
||
const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
|
||
if (
|
||
monthDiff < 0 ||
|
||
(monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())
|
||
) {
|
||
age--;
|
||
}
|
||
return age >= 16;
|
||
}).length
|
||
: 0;
|
||
const _bCare = Number(bonus.care_insurance || 0);
|
||
const _bPension = Number(bonus.pension_insurance || 0);
|
||
const _bEmp = Number(bonus.employment_insurance || 0);
|
||
const _bChild = Number(bonus.child_support || 0);
|
||
const _bTotal = Number(bonus.total_bonus || 0);
|
||
const _bHRateFull =
|
||
_bTotal > 0 && _bHealth > 0
|
||
? (((_bHealth * 2) / _bTotal) * 100).toFixed(3)
|
||
: "-";
|
||
const _bHRateHalf =
|
||
_bTotal > 0 && _bHealth > 0
|
||
? ((_bHealth / _bTotal) * 100).toFixed(3)
|
||
: "-";
|
||
const _bCRateFull =
|
||
_bTotal > 0 && _bCare > 0
|
||
? (((_bCare * 2) / _bTotal) * 100).toFixed(3)
|
||
: null;
|
||
const _bCRateHalf =
|
||
_bTotal > 0 && _bCare > 0
|
||
? ((_bCare / _bTotal) * 100).toFixed(3)
|
||
: null;
|
||
const _bPRateFull =
|
||
_bTotal > 0 && _bPension > 0
|
||
? (((_bPension * 2) / _bTotal) * 100).toFixed(3)
|
||
: "-";
|
||
const _bPRateHalf =
|
||
_bTotal > 0 && _bPension > 0
|
||
? ((_bPension / _bTotal) * 100).toFixed(3)
|
||
: "-";
|
||
const _bEmpRate =
|
||
_bTotal > 0 && _bEmp > 0
|
||
? ((_bEmp / _bTotal) * 100).toFixed(3)
|
||
: "-";
|
||
const _bCSRateFull =
|
||
_bTotal > 0 && _bChild > 0
|
||
? (((_bChild * 2) / _bTotal) * 100).toFixed(4)
|
||
: null;
|
||
const _bCSRateHalf =
|
||
_bTotal > 0 && _bChild > 0
|
||
? ((_bChild / _bTotal) * 100).toFixed(4)
|
||
: null;
|
||
const _bSocialSub = _bHealth + _bCare + _bPension + _bEmp + _bChild;
|
||
const _bHealthCareChild = _bHealth + _bCare + _bChild;
|
||
const _bTaxable = _bTotal - _bSocialSub;
|
||
|
||
const html = `
|
||
<h2>${bonus.bonus_year}年${
|
||
bonus.bonus_month
|
||
}月 賞与明細</h2>
|
||
<p><strong>従業員:</strong> ${employee.employee_code} - ${
|
||
employee.name
|
||
} | <strong>支給日:</strong> ${
|
||
bonus.payment_date
|
||
} | <strong>賞与種別:</strong> ${bonus.bonus_type}</p>
|
||
|
||
<div class="detail-section">
|
||
<h3>支給項目</h3>
|
||
<div class="detail-row"><span>基本賞与額:</span><span>¥${Number(
|
||
bonus.base_bonus || 0,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>業績賞与:</span><span>¥${Number(
|
||
bonus.performance_bonus || 0,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他賞与:</span><span>¥${Number(
|
||
bonus.other_bonus || 0,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総支給額:</span><span>¥${Number(
|
||
bonus.total_bonus || 0,
|
||
).toLocaleString()}</span></div>
|
||
</div>
|
||
|
||
<div class="detail-section">
|
||
<h3>控除項目</h3>
|
||
<div class="detail-row"><span>健康保険:</span><span>¥${_bHealth.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bHRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||
個人負担率:${_bHRateHalf}% ※協会けんぽ東京
|
||
</div>
|
||
<div class="detail-row"><span>介護保険:</span><span>¥${_bCare.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${
|
||
_bCRateFull
|
||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCRateFull}%(保険料率・全体)÷ 2(折半)<br>個人負担率:${_bCRateHalf}% ※40歳以上65歳未満が対象`
|
||
: "介護保険非該当(40歳未満または65歳以上)"
|
||
}
|
||
</div>
|
||
<div class="detail-row"><span>厚生年金:</span><span>¥${_bPension.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bPRateFull}%(保険料率・全体)÷ 2(折半)<br>
|
||
個人負担率:${_bPRateHalf}%(固定) ※上限:標準賞与額150万円/月
|
||
</div>
|
||
<div class="detail-row"><span>雇用保険:</span><span>¥${_bEmp.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${
|
||
_bEmp > 0
|
||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bEmpRate}%(雇用保険料率・従業員負担分)`
|
||
: "賞与に対する雇用保険は別途計算方式による(または0円)"
|
||
}
|
||
</div>
|
||
<div class="detail-row"><span>子ども・子育て支援金:</span><span>¥${_bChild.toLocaleString()}</span></div>
|
||
<div class="calc-note">
|
||
${
|
||
_bCSRateFull
|
||
? `¥${_bTotal.toLocaleString()}(賞与総支給額)× ${_bCSRateFull}%(拠出金率・全体)÷ 2(折半)<br>個人負担率:${_bCSRateHalf}% ※令和8年5月支払分(4月分)より控除開始`
|
||
: "令和8年5月支払分(4月分保険料)より控除開始 → 当賞与は対象外"
|
||
}
|
||
</div>
|
||
<div class="calc-note" style="background:#eef6ee; border-left:3px solid #28a745; padding:6px 10px; margin:6px 0; font-size:0.88em; color:#1a5c1a; line-height:1.7">
|
||
健康保険 ¥${_bHealth.toLocaleString()} + 介護保険 ¥${_bCare.toLocaleString()} + 子ども・子育て支援金 ¥${_bChild.toLocaleString()} = <strong>¥${_bHealthCareChild.toLocaleString()}</strong><br>
|
||
¥${_bHealthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_bPension.toLocaleString()}${' '}${_bEmp > 0 ? `+ 雇用保険 ¥${_bEmp.toLocaleString()}` : ''}= <strong>社会保険小計 ¥${_bSocialSub.toLocaleString()}</strong>
|
||
</div>
|
||
<div class="detail-row"><span style="font-weight:bold">社会保険小計:</span><span style="font-weight:bold">¥${_bSocialSub.toLocaleString()}</span></div>
|
||
<div class="detail-row">
|
||
<span>所得税 <small style="color:#666;">(甲欄${bDependentCount}人)</small>:</span>
|
||
<span>¥${Number(bonus.income_tax || 0).toLocaleString()}</span>
|
||
</div>
|
||
<div class="calc-note">
|
||
課税対象額 = 賞与総支給額 ¥${_bTotal.toLocaleString()} − 社会保険料計 ¥${_bSocialSub.toLocaleString()} = ¥${_bTaxable.toLocaleString()}<br>
|
||
→ 賞与に対する源泉徴収税額の算出率の表(甲欄・扶養${bDependentCount}人)を参照して計算
|
||
</div>
|
||
<div class="detail-row"><span>住民税:</span><span>¥${Number(
|
||
bonus.resident_tax || 0,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row"><span>その他控除:</span><span>¥${Number(
|
||
bonus.other_deduction || 0,
|
||
).toLocaleString()}</span></div>
|
||
<div class="detail-row total-row"><span>総控除額:</span><span>¥${Number(
|
||
bonus.total_deduction || 0,
|
||
).toLocaleString()}</span></div>
|
||
</div>
|
||
|
||
<div class="detail-section" style="background:#e7f3ff;">
|
||
<div class="detail-row total-row" style="font-size:1.5em;">
|
||
<span>差引支給額:</span>
|
||
<span>¥${Number(
|
||
bonus.net_bonus || 0,
|
||
).toLocaleString()}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="btn btn-primary" onclick="editBonus(${
|
||
bonus.bonus_id
|
||
})">編集</button>
|
||
<button class="btn btn-primary" onclick="recalculateBonus(${
|
||
bonus.bonus_id
|
||
})">再計算</button>
|
||
<button class="btn btn-secondary" onclick="document.getElementById('bonusDetail').style.display='none'; var sr=document.getElementById('splitRightBonus'); if(sr) sr.style.display='none';">閉じる</button>
|
||
`;
|
||
|
||
document.getElementById("bonusDetail").innerHTML = html;
|
||
const splitRightBonus = document.getElementById("splitRightBonus");
|
||
if (splitRightBonus) splitRightBonus.style.display = "block";
|
||
document.getElementById("bonusDetail").style.display = "block";
|
||
// モバイルでは明細パネルへ自動スクロール
|
||
if (window.innerWidth <= 767 && splitRightBonus) {
|
||
setTimeout(function() { splitRightBonus.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50);
|
||
}
|
||
} catch (error) {
|
||
alert("賞与明細の取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function editBonus(bonusId) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`);
|
||
const bonus = await response.json();
|
||
|
||
const html = `
|
||
<div style="position: fixed; top: 50px; left: 50%; transform: translateX(-50%); width: min(600px, 92vw); 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; box-sizing: border-box;">
|
||
<h2>賞与データ編集</h2>
|
||
<form id="editBonusForm" onsubmit="saveBonusEdit(event, ${bonusId})">
|
||
<div class="detail-section">
|
||
<h3>基本情報</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>支給日</label>
|
||
<input type="date" name="payment_date" value="${bonus.payment_date}" required />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>賞与種別</label>
|
||
<select name="bonus_type">
|
||
<option value="夏季賞与" ${bonus.bonus_type === "夏季賞与" ? "selected" : ""}>夏季賞与</option>
|
||
<option value="冬季賞与" ${bonus.bonus_type === "冬季賞与" ? "selected" : ""}>冬季賞与</option>
|
||
<option value="決算賞与" ${bonus.bonus_type === "決算賞与" ? "selected" : ""}>決算賞与</option>
|
||
<option value="その他" ${bonus.bonus_type === "その他" ? "selected" : ""}>その他</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="detail-section">
|
||
<h3>支給項目</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>基本賞与額</label>
|
||
<input type="number" name="base_bonus" step="1" value="${bonus.base_bonus}" required />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>業績賞与</label>
|
||
<input type="number" name="performance_bonus" step="1" value="${bonus.performance_bonus}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>その他賞与</label>
|
||
<input type="number" name="other_bonus" step="1" value="${bonus.other_bonus}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="detail-section">
|
||
<h3>控除項目(手動入力)</h3>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>その他控除</label>
|
||
<input type="number" name="other_deduction" step="1" value="${bonus.other_deduction}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p style="font-size:12px; color:#666;">※保存後に再計算を行うと社会保険料・所得税が自動更新されます。</p>
|
||
<button type="submit" class="btn btn-primary">保存して再計算</button>
|
||
<button type="button" class="btn btn-secondary" onclick="this.closest('div').parentElement.remove()">キャンセル</button>
|
||
</form>
|
||
</div>
|
||
`;
|
||
|
||
const editDiv = document.createElement("div");
|
||
editDiv.innerHTML = html;
|
||
document.body.appendChild(editDiv);
|
||
} catch (error) {
|
||
alert("賞与データの取得に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function saveBonusEdit(event, bonusId) {
|
||
event.preventDefault();
|
||
const form = event.target;
|
||
const formData = new FormData(form);
|
||
const data = {};
|
||
formData.forEach((value, key) => {
|
||
data[key] = isNaN(value) || value === "" ? value : Number(value);
|
||
});
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(data),
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert("賞与データを更新しました");
|
||
form.closest("div").parentElement.remove();
|
||
await recalculateBonus(bonusId, true);
|
||
} else {
|
||
const error = await response.json();
|
||
alert("更新に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("更新に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function deleteBonus(bonusId) {
|
||
if (
|
||
!confirm("この賞与データを削除しますか?\nこの操作は取り消せません。")
|
||
)
|
||
return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/payroll/bonus/${bonusId}`, {
|
||
method: "DELETE",
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert("賞与データを削除しました");
|
||
document.getElementById("bonusDetail").style.display = "none";
|
||
loadBonus();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(
|
||
"削除に失敗しました: " + JSON.stringify(error.detail || error),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
alert("削除に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function recalculateBonus(bonusId, skipConfirm = false) {
|
||
if (!skipConfirm && !confirm("賞与を再計算しますか?")) return;
|
||
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/bonus/${bonusId}/recalculate`,
|
||
{
|
||
method: "POST",
|
||
},
|
||
);
|
||
|
||
if (response.ok) {
|
||
alert("再計算が完了しました");
|
||
await loadBonus();
|
||
viewBonus(bonusId);
|
||
} else {
|
||
const error = await response.json();
|
||
alert("再計算に失敗しました: " + error.detail);
|
||
}
|
||
} catch (error) {
|
||
alert("再計算に失敗しました");
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// =====================================================
|
||
// 账票出力機能
|
||
// =====================================================
|
||
let voucherEmployeeSelection = {}; // 従業員選択状態を記録
|
||
|
||
async function loadVoucherEmployees() {
|
||
try {
|
||
const response = await fetch(
|
||
`${API_BASE}/payroll/vouchers/employees`,
|
||
);
|
||
const data = await response.json();
|
||
const employees = data.employees || [];
|
||
|
||
// 従業員選択を初期化
|
||
voucherEmployeeSelection = {};
|
||
|
||
// 従業員リストをHTMLで表示
|
||
const html = employees
|
||
.map((emp) => {
|
||
return `
|
||
<label style="display: flex; align-items: center; padding: 8px; cursor: pointer;">
|
||
<input type="checkbox"
|
||
class="voucherEmployeeCheckbox"
|
||
value="${emp.id}"
|
||
onchange="updateVoucherEmployeeCount()"
|
||
style="margin-right: 10px; cursor: pointer;">
|
||
<span>${emp.code} - ${emp.name}</span>
|
||
</label>
|
||
`;
|
||
})
|
||
.join("");
|
||
|
||
document.getElementById("voucherEmployeeList").innerHTML =
|
||
html || "<p>従業員がありません</p>";
|
||
|
||
// 選択数を0にリセット
|
||
document.getElementById("voucherEmployeeCount").textContent =
|
||
"選択数: 0";
|
||
} catch (error) {
|
||
console.error("従業員リスト取得エラー:", error);
|
||
document.getElementById("voucherEmployeeList").innerHTML =
|
||
"<p style='color: red;'>従業員リスト読み込みエラー</p>";
|
||
}
|
||
}
|
||
|
||
function updateVoucherEmployeeCount() {
|
||
const checkboxes = document.querySelectorAll(
|
||
".voucherEmployeeCheckbox:checked",
|
||
);
|
||
const count = checkboxes.length;
|
||
document.getElementById("voucherEmployeeCount").textContent =
|
||
`選択数: ${count}`;
|
||
}
|
||
|
||
function selectAllVoucherEmployees() {
|
||
document
|
||
.querySelectorAll(".voucherEmployeeCheckbox")
|
||
.forEach((cb) => (cb.checked = true));
|
||
updateVoucherEmployeeCount();
|
||
}
|
||
|
||
function clearAllVoucherEmployees() {
|
||
document
|
||
.querySelectorAll(".voucherEmployeeCheckbox")
|
||
.forEach((cb) => (cb.checked = false));
|
||
updateVoucherEmployeeCount();
|
||
}
|
||
|
||
function getSelectedVoucherEmployees() {
|
||
const checkboxes = document.querySelectorAll(
|
||
".voucherEmployeeCheckbox:checked",
|
||
);
|
||
return Array.from(checkboxes).map((cb) => parseInt(cb.value));
|
||
}
|
||
|
||
async function printVouchers() {
|
||
// 検証
|
||
const startYear = Number(
|
||
document.getElementById("voucherStartYear").value,
|
||
);
|
||
const startMonth = Number(
|
||
document.getElementById("voucherStartMonth").value,
|
||
);
|
||
if (!startYear || !startMonth) {
|
||
alert("開始年月を選択してください");
|
||
return;
|
||
}
|
||
|
||
const employeeIds = getSelectedVoucherEmployees();
|
||
if (employeeIds.length === 0) {
|
||
alert("最少1人の従業員を選択してください");
|
||
return;
|
||
}
|
||
|
||
// 年月を解析
|
||
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;
|
||
|
||
// APIを呼び出す
|
||
try {
|
||
const payload = {
|
||
start_year: startYear,
|
||
start_month: startMonth,
|
||
end_year: endYear,
|
||
end_month: endMonth,
|
||
employee_ids: employeeIds,
|
||
voucher_type: voucherType,
|
||
format: "preview",
|
||
};
|
||
|
||
console.log("送信ペイロード:", JSON.stringify(payload, null, 2));
|
||
|
||
const response = await fetch(`${API_BASE}/payroll/vouchers/export`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
|
||
const result = await response.json();
|
||
console.log("レスポンス:", result);
|
||
|
||
if (!result.has_data) {
|
||
alert(result.message || "データが見つかりません");
|
||
return;
|
||
}
|
||
|
||
// 新しいウィンドウを開く
|
||
const printWindow = window.open("", "", "height=1600,width=1800");
|
||
|
||
// 打印用HTML构建
|
||
let printHtml = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>給与・賞与明細</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; }
|
||
body {
|
||
font-family: 'MS Gothic', 'Courier New', monospace;
|
||
font-size: 12px;
|
||
color: #333;
|
||
}
|
||
.page {
|
||
page-break-after: always;
|
||
padding: 30px 40px;
|
||
min-height: 1000px;
|
||
}
|
||
.page:last-child {
|
||
page-break-after: auto;
|
||
}
|
||
.page-title {
|
||
text-align: center;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
}
|
||
.employee-info {
|
||
font-size: 14px;
|
||
margin-bottom: 5px;
|
||
}
|
||
.detail-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 15px;
|
||
}
|
||
.detail-table td {
|
||
border: 1px solid #333;
|
||
padding: 6px 8px;
|
||
}
|
||
.detail-table .label {
|
||
background: #f5f5f5;
|
||
font-weight: bold;
|
||
width: 40%;
|
||
}
|
||
.detail-table .value {
|
||
text-align: right;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
.section-title {
|
||
font-weight: bold;
|
||
margin-top: 10px;
|
||
margin-bottom: 5px;
|
||
border-bottom: 1px solid #333;
|
||
padding-bottom: 2px;
|
||
}
|
||
.total-row {
|
||
background: #f0f0f0;
|
||
font-weight: bold;
|
||
}
|
||
@media print {
|
||
body { margin: 0; padding: 0; }
|
||
.page { padding: 20px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
`;
|
||
|
||
// 月末日を計算する関数
|
||
function getLastDayOfMonth(year, month) {
|
||
const date = new Date(year, month, 0);
|
||
return String(date.getDate()).padStart(2, "0");
|
||
}
|
||
|
||
const generationDate = new Date().toLocaleDateString("ja-JP");
|
||
|
||
// グループ化: 従業員ごと、年月ごと、給与/賞与ごと
|
||
let currentKey = null;
|
||
let pageOpen = false;
|
||
|
||
if (result.summary && Array.isArray(result.summary)) {
|
||
result.summary.forEach((emp) => {
|
||
if (!emp.has_salary_data && !emp.has_bonus_data) return;
|
||
|
||
const salaries = (result.data?.salary || []).filter(
|
||
(s) => s.employee_id === emp.employee_id,
|
||
);
|
||
const bonuses = (result.data?.bonus || []).filter(
|
||
(b) => b.employee_id === emp.employee_id,
|
||
);
|
||
|
||
// 給与データの処理
|
||
salaries.forEach((salary) => {
|
||
const salaryYearMonth = `${salary.payroll_year}-${salary.payroll_month}`;
|
||
const currentKey_new = `${emp.employee_id}-${salaryYearMonth}-salary`;
|
||
const paymentDate = `${salary.payroll_year}年${String(salary.payroll_month).padStart(2, "0")}月${getLastDayOfMonth(salary.payroll_year, salary.payroll_month)}日`;
|
||
|
||
// 新しいキーが異なる場合は新しいページを開く
|
||
if (currentKey !== currentKey_new) {
|
||
if (pageOpen) {
|
||
printHtml += "</div>";
|
||
}
|
||
currentKey = currentKey_new;
|
||
printHtml += '<div class="page">';
|
||
printHtml += `<div style="font-size: 11px; color: #999; margin-bottom: 10px; text-align: right;">帳票生成日: ${generationDate}</div>`;
|
||
pageOpen = true;
|
||
}
|
||
|
||
printHtml += `
|
||
<div class="page-title">${salary.payroll_year}年${String(salary.payroll_month).padStart(2, "0")}月 給与明細</div>
|
||
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
|
||
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${paymentDate}</div>
|
||
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
|
||
|
||
<div class="section-title">勤怠情報</div>
|
||
<table class="detail-table">
|
||
<tr>
|
||
<td class="label">出勤日数</td>
|
||
<td class="value">${Number(salary.working_days).toFixed(1)}日</td>
|
||
<td class="label">勤務時間</td>
|
||
<td class="value">${Number(salary.working_hours).toFixed(1)}時間</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">残業時間</td>
|
||
<td class="value">${Number(salary.overtime_hours).toFixed(1)}時間</td>
|
||
<td class="label">休日出勤</td>
|
||
<td class="value">${Number(salary.holiday_hours).toFixed(1)}時間</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">遅刻時間</td>
|
||
<td class="value">${Number(salary.late_hours).toFixed(1)}時間</td>
|
||
<td class="label">欠勤日数</td>
|
||
<td class="value">${Number(salary.absent_days).toFixed(1)}日</td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="section-title">支給項目</div>
|
||
<table class="detail-table">
|
||
<tr>
|
||
<td class="label">基本給</td>
|
||
<td class="value">¥${Number(salary.base_salary).toLocaleString()}</td>
|
||
<td class="label">残業手当</td>
|
||
<td class="value">¥${Number(salary.overtime_pay).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">休日手当</td>
|
||
<td class="value">¥${Number(salary.holiday_pay).toLocaleString()}</td>
|
||
<td class="label">通勤手当</td>
|
||
<td class="value">¥${Number(salary.commute_allowance).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">その他手当</td>
|
||
<td class="value">¥${Number(salary.other_allowance).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
<tr class="total-row">
|
||
<td class="label">総支給額</td>
|
||
<td class="value">¥${Number(salary.total_payment).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="section-title">控除項目</div>
|
||
<table class="detail-table">
|
||
<tr>
|
||
<td class="label">健康保険</td>
|
||
<td class="value">¥${Number(salary.health_insurance).toLocaleString()}</td>
|
||
<td class="label">介護保険</td>
|
||
<td class="value">¥${Number(salary.care_insurance).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">厚生年金</td>
|
||
<td class="value">¥${Number(salary.pension_insurance).toLocaleString()}</td>
|
||
<td class="label">雇用保険</td>
|
||
<td class="value">¥${Number(salary.employment_insurance).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">子ども・子育て支援金</td>
|
||
<td class="value">¥${Number(salary.child_support || 0).toLocaleString()}</td>
|
||
<td class="label">所得税</td>
|
||
<td class="value">¥${Number(salary.income_tax).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">住民税</td>
|
||
<td class="value">¥${Number(salary.resident_tax).toLocaleString()}</td>
|
||
<td class="label">その他控除</td>
|
||
<td class="value">¥${Number(salary.other_deduction).toLocaleString()}</td>
|
||
</tr>
|
||
<tr class="total-row">
|
||
<td class="label">総控除額</td>
|
||
<td class="value">¥${Number(salary.total_deduction).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div style="margin-top: 20px; padding: 15px; border: 2px solid #000; text-align: right;">
|
||
<div style="font-size: 14px; font-weight: bold;">差引支給額</div>
|
||
<div style="font-size: 24px; font-weight: bold; margin-top: 5px;">¥${Number(salary.net_payment).toLocaleString()}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// 賞与データの処理
|
||
bonuses.forEach((bonus) => {
|
||
const bonusYearMonth = `${bonus.bonus_year}-${bonus.bonus_month}`;
|
||
const currentKey_new = `${emp.employee_id}-${bonusYearMonth}-bonus`;
|
||
// DBに保存された実際の支給日を使用。なければ月末日を計算
|
||
const bonusPaymentDate = bonus.payment_date
|
||
? (() => {
|
||
const d = new Date(bonus.payment_date);
|
||
return `${d.getFullYear()}年${String(d.getMonth() + 1).padStart(2, "0")}月${String(d.getDate()).padStart(2, "0")}日`;
|
||
})()
|
||
: `${bonus.bonus_year}年${String(bonus.bonus_month).padStart(2, "0")}月${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}日`;
|
||
if (currentKey && currentKey !== currentKey_new) {
|
||
if (pageOpen) {
|
||
printHtml += "</div>";
|
||
}
|
||
currentKey = currentKey_new;
|
||
printHtml += '<div class="page">';
|
||
printHtml += `<div style="font-size: 11px; color: #999; margin-bottom: 10px; text-align: right;">帳票生成日: ${generationDate}</div>`;
|
||
pageOpen = true;
|
||
}
|
||
|
||
printHtml += `
|
||
<div class="page-title">${bonus.bonus_year}年${String(bonus.bonus_month).padStart(2, "0")}月 賞与明細${bonus.bonus_type ? ` (${bonus.bonus_type})` : ""}</div>
|
||
<div class="employee-info">従業員: ${emp.employee_code} ${emp.employee_name}</div>
|
||
<div class="employee-info" style="color: #666; font-size: 12px;">支給日: ${bonusPaymentDate}</div>
|
||
${emp.employment_type ? `<div class="employee-info" style="color: #666; font-size: 12px;">雇用形態:${emp.employment_type}</div>` : ""}
|
||
|
||
<div class="section-title">支給項目</div>
|
||
<table class="detail-table">
|
||
<tr>
|
||
<td class="label">基本賞与額</td>
|
||
<td class="value">¥${Number(bonus.base_bonus).toLocaleString()}</td>
|
||
<td class="label">業績賞与</td>
|
||
<td class="value">¥${Number(bonus.performance_bonus).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">その他賞与</td>
|
||
<td class="value">¥${Number(bonus.other_bonus).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
<tr class="total-row">
|
||
<td class="label">総支給額</td>
|
||
<td class="value">¥${Number(bonus.total_bonus).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div class="section-title">控除項目</div>
|
||
<table class="detail-table">
|
||
<tr>
|
||
<td class="label">健康保険</td>
|
||
<td class="value">¥${Number(bonus.health_insurance).toLocaleString()}</td>
|
||
<td class="label">介護保険</td>
|
||
<td class="value">¥${Number(bonus.care_insurance).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">厚生年金</td>
|
||
<td class="value">¥${Number(bonus.pension_insurance).toLocaleString()}</td>
|
||
<td class="label">雇用保険</td>
|
||
<td class="value">¥${Number(bonus.employment_insurance).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">子ども・子育て支援金</td>
|
||
<td class="value">¥${Number(bonus.child_support || 0).toLocaleString()}</td>
|
||
<td class="label">所得税</td>
|
||
<td class="value">¥${Number(bonus.income_tax).toLocaleString()}</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="label">その他控除</td>
|
||
<td class="value">¥${Number(bonus.other_deduction).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
<tr class="total-row">
|
||
<td class="label">総控除額</td>
|
||
<td class="value">¥${Number(bonus.total_deduction).toLocaleString()}</td>
|
||
<td class="label"></td>
|
||
<td class="value"></td>
|
||
</tr>
|
||
</table>
|
||
|
||
<div style="margin-top: 20px; padding: 15px; border: 2px solid #000; text-align: right;">
|
||
<div style="font-size: 14px; font-weight: bold;">差引支給額</div>
|
||
<div style="font-size: 24px; font-weight: bold; margin-top: 5px;">¥${Number(bonus.net_bonus).toLocaleString()}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
});
|
||
|
||
if (pageOpen) {
|
||
printHtml += "</div>";
|
||
}
|
||
}
|
||
|
||
printHtml += `
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
// 打印内容を設定して打印
|
||
printWindow.document.write(printHtml);
|
||
printWindow.document.close();
|
||
|
||
// 少し遅延させて打印ダイアログを表示
|
||
setTimeout(() => {
|
||
printWindow.print();
|
||
}, 1000);
|
||
} catch (error) {
|
||
console.error("エラー:", error);
|
||
alert("印刷準備中にエラーが発生しました");
|
||
}
|
||
}
|
||
|
||
function showNoDataAlert(message) {
|
||
document.getElementById("noDataMessage").textContent = message;
|
||
document.getElementById("noDataModal").style.display = "flex";
|
||
}
|
||
|
||
async function exportVouchersCSV() {
|
||
// 検証
|
||
const startYear = Number(
|
||
document.getElementById("voucherStartYear").value,
|
||
);
|
||
const startMonth = Number(
|
||
document.getElementById("voucherStartMonth").value,
|
||
);
|
||
if (!startYear || !startMonth) {
|
||
alert("開始年月を選択してください");
|
||
return;
|
||
}
|
||
|
||
const employeeIds = getSelectedVoucherEmployees();
|
||
if (employeeIds.length === 0) {
|
||
alert("最少1人の従業員を選択してください");
|
||
return;
|
||
}
|
||
|
||
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;
|
||
|
||
try {
|
||
const payload = {
|
||
start_year: startYear,
|
||
start_month: startMonth,
|
||
end_year: endYear,
|
||
end_month: endMonth,
|
||
employee_ids: employeeIds,
|
||
voucher_type: voucherType,
|
||
format: "csv",
|
||
};
|
||
|
||
const response = await fetch(`${API_BASE}/payroll/vouchers/export`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.has_data) {
|
||
alert(result.message || "データが見つかりません");
|
||
return;
|
||
}
|
||
|
||
// CSVデータを生成
|
||
let csvContent =
|
||
"データ,従業員コード,従業員名,年月,支給額,控除額,差引支給額\n";
|
||
|
||
// 給与データ
|
||
if (result.data.salary) {
|
||
(result.data.salary || []).forEach((salary) => {
|
||
csvContent += `給与,${salary.employee_code},${salary.employee_name},${salary.payroll_year}-${String(salary.payroll_month).padStart(2, "0")},${salary.total_payment},${salary.total_deduction},${salary.net_payment}\n`;
|
||
});
|
||
}
|
||
|
||
// 賞与データ
|
||
if (result.data.bonus) {
|
||
(result.data.bonus || []).forEach((bonus) => {
|
||
csvContent += `賞与,${bonus.employee_code},${bonus.employee_name},${bonus.bonus_year}-${String(bonus.bonus_month).padStart(2, "0")},${bonus.total_bonus},${bonus.total_deduction},${bonus.net_bonus}\n`;
|
||
});
|
||
}
|
||
|
||
// ダウンロード
|
||
const blob = new Blob([csvContent], {
|
||
type: "text/csv;charset=utf-8;",
|
||
});
|
||
const link = document.createElement("a");
|
||
const url = URL.createObjectURL(blob);
|
||
link.setAttribute("href", url);
|
||
link.setAttribute("download", `vouchers_${new Date().getTime()}.csv`);
|
||
link.style.visibility = "hidden";
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
alert("CSVを出力しました");
|
||
} catch (error) {
|
||
console.error("CSVエラー:", error);
|
||
alert("CSV出力に失敗しました");
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|