606 lines
20 KiB
HTML
606 lines
20 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>
|