Files
njts-accounting-core/backend/app/static/payroll-calculation.html
2026-01-20 11:15:49 +09:00

435 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css">
<style>
.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;
}
.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;
}
</style>
</head>
<body>
<div class="container">
<h1>給与管理 - 月次給与計算</h1>
<div class="filters">
<label>
対象年月:
<input type="number" id="filterYear" style="width: 100px;" placeholder="年">
</label>
<label>
<input type="number" id="filterMonth" style="width: 80px;" min="1" max="12" placeholder="月">
</label>
<button class="btn btn-primary" onclick="loadPayrolls()">検索</button>
<button class="btn btn-success" onclick="showCalculateForm()">新規計算</button>
</div>
<div id="payrollList" class="payroll-list"></div>
<!-- 給与計算フォーム(モーダル風) -->
<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="number" name="payroll_year" required>
</div>
<div class="form-group">
<label>対象月 *</label>
<input type="number" name="payroll_month" min="1" max="12" required>
</div>
<div class="form-group">
<label>支給日 *</label>
<input type="date" name="payment_date" required>
</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>
<button type="submit" class="btn btn-primary">計算実行</button>
<button type="button" class="btn btn-secondary" onclick="closeCalculateForm()">キャンセル</button>
</form>
</div>
<!-- 給与明細表示 -->
<div id="payrollDetail" class="payroll-detail" style="display:none;"></div>
</div>
<script>
const API_BASE = 'http://localhost:8000';
async function loadEmployees() {
const response = await fetch(`${API_BASE}/payroll/employees/?is_active=true`);
const employees = await response.json();
const select = document.getElementById('employeeSelect');
select.innerHTML = '<option value="">選択してください</option>' +
employees.map(emp => `<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`).join('');
}
async function loadPayrolls() {
const year = document.getElementById('filterYear').value;
const month = document.getElementById('filterMonth').value;
const params = new URLSearchParams();
if (year) params.append('payroll_year', year);
if (month) params.append('payroll_month', month);
try {
const response = await fetch(`${API_BASE}/payroll/calculation/?${params}`);
const payrolls = await response.json();
const html = payrolls.map(p => `
<div class="payroll-item" onclick="viewPayroll(${p.payroll_id})">
<div>
<strong>${p.payroll_year}${p.payroll_month}月</strong>
<span class="status-badge status-${p.status}">${getStatusLabel(p.status)}</span>
</div>
<div>
従業員ID: ${p.employee_id} | 支給日: ${p.payment_date}
</div>
<div>
<strong>差引支給額: ¥${Number(p.net_payment).toLocaleString()}</strong>
(総支給: ¥${Number(p.total_payment).toLocaleString()} - 控除: ¥${Number(p.total_deduction).toLocaleString()})
</div>
</div>
`).join('');
document.getElementById('payrollList').innerHTML = html || '<p>給与データがありません</p>';
} 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);
const data = {};
formData.forEach((value, key) => {
data[key] = isNaN(value) || value === '' ? value : Number(value);
});
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();
viewPayroll(result.payroll_id);
} else {
const error = await response.json();
alert('計算に失敗しました: ' + error.detail);
}
} 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 html = `
<h2>${payroll.payroll_year}${payroll.payroll_month}月 給与明細</h2>
<p>従業員ID: ${payroll.employee_id} | 支給日: ${payroll.payment_date}</p>
<p>ステータス: <span class="status-badge status-${payroll.status}">${getStatusLabel(payroll.status)}</span></p>
<div class="detail-section">
<h3>勤怠情報</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>
<div class="detail-section">
<h3>控除項目</h3>
<div class="detail-row"><span>健康保険:</span><span>¥${Number(payroll.health_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>厚生年金:</span><span>¥${Number(payroll.pension_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>雇用保険:</span><span>¥${Number(payroll.employment_insurance).toLocaleString()}</span></div>
<div class="detail-row"><span>所得税:</span><span>¥${Number(payroll.income_tax).toLocaleString()}</span></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'">閉じる</button>
`;
document.getElementById('payrollDetail').innerHTML = html;
document.getElementById('payrollDetail').style.display = 'block';
document.getElementById('payrollDetail').scrollIntoView({ behavior: 'smooth' });
} 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) {
if (!confirm('給与を再計算しますか?')) return;
try {
const response = await fetch(`${API_BASE}/payroll/calculation/${payrollId}/recalculate`, {
method: 'POST'
});
if (response.ok) {
alert('再計算が完了しました');
viewPayroll(payrollId);
} else {
const error = await response.json();
alert('再計算に失敗しました: ' + error.detail);
}
} catch (error) {
alert('再計算に失敗しました');
console.error(error);
}
}
// 初期化
const now = new Date();
document.getElementById('filterYear').value = now.getFullYear();
document.getElementById('filterMonth').value = now.getMonth() + 1;
loadPayrolls();
</script>
</body>
</html>