930 lines
29 KiB
HTML
930 lines
29 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>
|
||
<script src="js/auth.js"></script>
|
||
<link rel="stylesheet" href="css/style.css" />
|
||
<link rel="stylesheet" href="/css/mobile.css" />
|
||
<style>
|
||
body {
|
||
font-family:
|
||
system-ui,
|
||
-apple-system,
|
||
"Yu Gothic UI",
|
||
"Hiragino Kaku Gothic ProN",
|
||
"メイリオ",
|
||
sans-serif;
|
||
padding: 24px;
|
||
background: #f5f7fa;
|
||
}
|
||
.page-header {
|
||
max-width: 1100px;
|
||
margin: 0 auto 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.page-header h1 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
}
|
||
.back-link {
|
||
padding: 8px 16px;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
text-decoration: none;
|
||
}
|
||
.back-link:hover {
|
||
background: #0056b3;
|
||
}
|
||
|
||
/* ── アップロードゾーン ── */
|
||
.upload-zone {
|
||
max-width: 1100px;
|
||
margin: 0 auto 24px;
|
||
border: 2px dashed #adb5bd;
|
||
border-radius: 10px;
|
||
padding: 28px 24px;
|
||
text-align: center;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
transition:
|
||
border-color 0.2s,
|
||
background 0.2s;
|
||
}
|
||
.upload-zone.dragover {
|
||
border-color: #007bff;
|
||
background: #e8f4ff;
|
||
}
|
||
.upload-zone h3 {
|
||
margin: 0 0 8px;
|
||
font-size: 16px;
|
||
color: #495057;
|
||
}
|
||
.upload-zone p {
|
||
margin: 0;
|
||
color: #6c757d;
|
||
font-size: 13px;
|
||
}
|
||
.upload-zone input[type="file"] {
|
||
display: none;
|
||
}
|
||
.upload-btn {
|
||
display: inline-block;
|
||
margin-top: 12px;
|
||
padding: 8px 20px;
|
||
background: #007bff;
|
||
color: #fff;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
border: none;
|
||
}
|
||
.upload-btn:hover {
|
||
background: #0056b3;
|
||
}
|
||
#upload-status {
|
||
max-width: 1100px;
|
||
margin: 0 auto 12px;
|
||
font-size: 14px;
|
||
min-height: 24px;
|
||
}
|
||
.status-ok {
|
||
color: #28a745;
|
||
font-weight: bold;
|
||
}
|
||
.status-err {
|
||
color: #dc3545;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* ── フィルターバー ── */
|
||
.filter-bar {
|
||
max-width: 1100px;
|
||
margin: 0 auto 16px;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.type-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.type-tab {
|
||
padding: 5px 14px;
|
||
border-radius: 20px;
|
||
border: 1px solid #ced4da;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: #495057;
|
||
transition: all 0.15s;
|
||
}
|
||
.type-tab:hover {
|
||
border-color: #007bff;
|
||
color: #007bff;
|
||
}
|
||
.type-tab.active {
|
||
background: #007bff;
|
||
color: #fff;
|
||
border-color: #007bff;
|
||
}
|
||
.filter-select {
|
||
padding: 5px 10px;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 5px;
|
||
font-size: 13px;
|
||
background: #fff;
|
||
}
|
||
|
||
/* ── 文書一覧テーブル ── */
|
||
.doc-table-wrap {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
|
||
overflow: hidden;
|
||
}
|
||
.doc-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 14px;
|
||
}
|
||
.doc-table thead th {
|
||
background: #f8f9fa;
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
border-bottom: 2px solid #dee2e6;
|
||
white-space: nowrap;
|
||
}
|
||
.doc-table tbody tr {
|
||
border-bottom: 1px solid #f0f0f0;
|
||
cursor: pointer;
|
||
transition: background 0.1s;
|
||
}
|
||
.doc-table tbody tr:hover {
|
||
background: #f0f7ff;
|
||
}
|
||
.doc-table tbody tr.selected {
|
||
background: #dbeafe;
|
||
}
|
||
.doc-table tbody td {
|
||
padding: 10px 12px;
|
||
}
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.badge-shakai {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
}
|
||
.badge-nounyu {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
.badge-shoyo {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
.badge-unknown {
|
||
background: #e2e3e5;
|
||
color: #383d41;
|
||
}
|
||
.no-data {
|
||
text-align: center;
|
||
color: #aaa;
|
||
padding: 40px;
|
||
}
|
||
|
||
/* ── 詳細パネル ── */
|
||
#detail-panel {
|
||
max-width: 1100px;
|
||
margin: 16px auto 0;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07);
|
||
padding: 24px;
|
||
display: none;
|
||
}
|
||
#detail-panel.visible {
|
||
display: block;
|
||
}
|
||
.detail-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 20px;
|
||
}
|
||
.detail-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
.detail-close {
|
||
background: none;
|
||
border: 1px solid #ced4da;
|
||
border-radius: 4px;
|
||
padding: 4px 10px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: #6c757d;
|
||
}
|
||
.detail-close:hover {
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
/* 事業所情報カード */
|
||
.info-cards {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20px;
|
||
}
|
||
.info-card {
|
||
flex: 1;
|
||
min-width: 180px;
|
||
background: #f8f9fa;
|
||
border-radius: 6px;
|
||
padding: 12px 16px;
|
||
}
|
||
.info-card .label {
|
||
font-size: 11px;
|
||
color: #6c757d;
|
||
margin-bottom: 4px;
|
||
}
|
||
.info-card .value {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #212529;
|
||
}
|
||
|
||
/* 金額テーブル */
|
||
.amount-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
.amount-section h3 {
|
||
font-size: 15px;
|
||
margin: 0 0 12px;
|
||
border-left: 3px solid #007bff;
|
||
padding-left: 8px;
|
||
}
|
||
.amount-grid {
|
||
display: grid;
|
||
gap: 10px;
|
||
}
|
||
.amount-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 14px;
|
||
background: #f8f9fa;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
}
|
||
.amount-row.total {
|
||
background: #e8f4ff;
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
}
|
||
.amount-row .amt {
|
||
font-family: monospace;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* 告知額 vs 領収済額 サイドバイサイド */
|
||
.nounyu-compare {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.nounyu-compare {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
.nounyu-box {
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
.nounyu-box.kokuchi {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffc107;
|
||
}
|
||
.nounyu-box.ryoshu {
|
||
background: #d4edda;
|
||
border: 1px solid #28a745;
|
||
}
|
||
.nounyu-box h4 {
|
||
margin: 0 0 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 賞与明細テーブル */
|
||
.shoyo-table-wrap {
|
||
overflow-x: auto;
|
||
}
|
||
.shoyo-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
.shoyo-table th {
|
||
background: #f8f9fa;
|
||
padding: 8px 10px;
|
||
text-align: center;
|
||
border: 1px solid #dee2e6;
|
||
white-space: nowrap;
|
||
font-size: 12px;
|
||
}
|
||
.shoyo-table td {
|
||
padding: 7px 10px;
|
||
border: 1px solid #f0f0f0;
|
||
text-align: right;
|
||
}
|
||
.shoyo-table td.name {
|
||
text-align: left;
|
||
font-weight: 600;
|
||
}
|
||
.shoyo-table tfoot td {
|
||
background: #e8f4ff;
|
||
font-weight: 700;
|
||
border-top: 2px solid #007bff;
|
||
}
|
||
.shoyo-table .empty-cell {
|
||
color: #ccc;
|
||
text-align: center;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="page-header">
|
||
<h1>社会保険電子文書ポータル</h1>
|
||
<a class="back-link" href="payroll.html">← 給与管理メニューに戻る</a>
|
||
</div>
|
||
|
||
<!-- アップロードゾーン -->
|
||
<div class="upload-zone" id="upload-zone">
|
||
<h3>📥 社会保険 ZIP ファイルをここにドロップ</h3>
|
||
<p>またはボタンから選択してください(複数ファイル対応)</p>
|
||
<input type="file" id="zip-input" accept=".zip" multiple />
|
||
<br />
|
||
<button
|
||
class="upload-btn"
|
||
onclick="document.getElementById('zip-input').click()"
|
||
>
|
||
ファイルを選択
|
||
</button>
|
||
</div>
|
||
<div id="upload-status"></div>
|
||
|
||
<!-- フィルターバー -->
|
||
<div class="filter-bar">
|
||
<div class="type-tabs" id="type-tabs">
|
||
<button class="type-tab active" data-type="">すべて</button>
|
||
<button class="type-tab" data-type="SHAKAI">社会保険料額情報</button>
|
||
<button class="type-tab" data-type="NOUNYU">納入告知・領収</button>
|
||
<button class="type-tab" data-type="SHOYO">賞与保険料算出</button>
|
||
</div>
|
||
<select class="filter-select" id="year-select">
|
||
<option value="">年度(すべて)</option>
|
||
</select>
|
||
<select class="filter-select" id="month-select">
|
||
<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="doc-table-wrap">
|
||
<table class="doc-table">
|
||
<thead>
|
||
<tr>
|
||
<th>種別</th>
|
||
<th>対象年月</th>
|
||
<th>事業所名</th>
|
||
<th>整理記号・番号</th>
|
||
<th>年金事務所</th>
|
||
<th>発行日</th>
|
||
<th>登録日時</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="doc-list-body">
|
||
<tr>
|
||
<td colspan="7" class="no-data">データを読み込んでいます...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 詳細パネル -->
|
||
<div id="detail-panel">
|
||
<div class="detail-header">
|
||
<h2 id="detail-title">詳細</h2>
|
||
<button class="detail-close" onclick="closeDetail()">✕ 閉じる</button>
|
||
</div>
|
||
<div id="detail-body"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = "";
|
||
let currentFilter = { type: "", year: "", month: "" };
|
||
let selectedDocId = null;
|
||
|
||
// ──────────────────────────────────
|
||
// ページ初期化
|
||
// ──────────────────────────────────
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
checkAuth();
|
||
loadDocuments();
|
||
loadFilterOptions();
|
||
initUploadZone();
|
||
|
||
document.getElementById("type-tabs").addEventListener("click", (e) => {
|
||
const tab = e.target.closest(".type-tab");
|
||
if (!tab) return;
|
||
document
|
||
.querySelectorAll(".type-tab")
|
||
.forEach((t) => t.classList.remove("active"));
|
||
tab.classList.add("active");
|
||
currentFilter.type = tab.dataset.type;
|
||
loadDocuments();
|
||
});
|
||
|
||
document
|
||
.getElementById("year-select")
|
||
.addEventListener("change", (e) => {
|
||
currentFilter.year = e.target.value;
|
||
loadDocuments();
|
||
});
|
||
|
||
document
|
||
.getElementById("month-select")
|
||
.addEventListener("change", (e) => {
|
||
currentFilter.month = e.target.value;
|
||
loadDocuments();
|
||
});
|
||
});
|
||
|
||
// ──────────────────────────────────
|
||
// 文書一覧ロード
|
||
// ──────────────────────────────────
|
||
async function loadDocuments() {
|
||
const params = new URLSearchParams();
|
||
if (currentFilter.type) params.set("doc_type", currentFilter.type);
|
||
if (currentFilter.year) params.set("year", currentFilter.year);
|
||
if (currentFilter.month) params.set("month", currentFilter.month);
|
||
|
||
const url = `${API}/nenkin/documents${params.toString() ? "?" + params : ""}`;
|
||
try {
|
||
const res = await fetch(url);
|
||
const docs = await res.json();
|
||
renderDocList(docs);
|
||
} catch (e) {
|
||
document.getElementById("doc-list-body").innerHTML =
|
||
`<tr><td colspan="7" class="no-data" style="color:#dc3545">読み込みエラー: ${e.message}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function renderDocList(docs) {
|
||
const tbody = document.getElementById("doc-list-body");
|
||
if (!docs.length) {
|
||
tbody.innerHTML =
|
||
'<tr><td colspan="7" class="no-data">文書が登録されていません</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = docs
|
||
.map((d) => {
|
||
const period =
|
||
d.purpose_year && d.purpose_month
|
||
? `${d.purpose_year}年${d.purpose_month}月`
|
||
: "−";
|
||
const issueDate = d.issue_date
|
||
? d.issue_date.replace(/-/g, "/")
|
||
: "−";
|
||
const uploadedAt = d.uploaded_at
|
||
? d.uploaded_at.slice(0, 16).replace("T", " ")
|
||
: "−";
|
||
const selected = d.id === selectedDocId ? "selected" : "";
|
||
return `<tr class="${selected}" onclick="loadDetail(${d.id})" data-id="${d.id}">
|
||
<td>${badgeHtml(d.document_type, d.document_type_name)}</td>
|
||
<td>${period}</td>
|
||
<td>${escHtml(d.office_name || "−")}</td>
|
||
<td><small>${escHtml(d.office_kigo || "")} ${escHtml(d.office_number || "")}</small></td>
|
||
<td>${escHtml(d.nenkin_jimusho || "−")}</td>
|
||
<td>${issueDate}</td>
|
||
<td><small>${uploadedAt}</small></td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
}
|
||
|
||
function badgeHtml(type, name) {
|
||
const cls =
|
||
{
|
||
SHAKAI: "badge-shakai",
|
||
NOUNYU: "badge-nounyu",
|
||
SHOYO: "badge-shoyo",
|
||
}[type] || "badge-unknown";
|
||
return `<span class="badge ${cls}">${escHtml(name || type)}</span>`;
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// フィルター選択肢(年度)
|
||
// ──────────────────────────────────
|
||
async function loadFilterOptions() {
|
||
try {
|
||
const res = await fetch(`${API}/nenkin/filter-options`);
|
||
const data = await res.json();
|
||
const sel = document.getElementById("year-select");
|
||
const years = [
|
||
...new Set(data.periods.map((p) => p.purpose_year)),
|
||
].sort((a, b) => b - a);
|
||
years.forEach((y) => {
|
||
const opt = document.createElement("option");
|
||
opt.value = y;
|
||
opt.textContent = `${y}年`;
|
||
sel.appendChild(opt);
|
||
});
|
||
} catch (_) {}
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// 詳細ロード
|
||
// ──────────────────────────────────
|
||
async function loadDetail(docId) {
|
||
// 選択行のハイライト
|
||
selectedDocId = docId;
|
||
document.querySelectorAll(".doc-table tbody tr").forEach((tr) => {
|
||
tr.classList.toggle("selected", parseInt(tr.dataset.id) === docId);
|
||
});
|
||
|
||
try {
|
||
const res = await fetch(`${API}/nenkin/documents/${docId}`);
|
||
const data = await res.json();
|
||
renderDetail(data);
|
||
} catch (e) {
|
||
showDetailError(e.message);
|
||
}
|
||
}
|
||
|
||
function closeDetail() {
|
||
selectedDocId = null;
|
||
document.getElementById("detail-panel").classList.remove("visible");
|
||
document
|
||
.querySelectorAll(".doc-table tbody tr")
|
||
.forEach((tr) => tr.classList.remove("selected"));
|
||
}
|
||
|
||
function renderDetail({ document: doc, detail }) {
|
||
const panel = document.getElementById("detail-panel");
|
||
const title = document.getElementById("detail-title");
|
||
const body = document.getElementById("detail-body");
|
||
|
||
const period =
|
||
doc.purpose_year && doc.purpose_month
|
||
? `${doc.purpose_year}年${doc.purpose_month}月分`
|
||
: "";
|
||
title.textContent = `${doc.document_type_name} ${period}`;
|
||
|
||
// 共通事業所カード
|
||
const infoHtml = `
|
||
<div class="info-cards">
|
||
<div class="info-card">
|
||
<div class="label">事業所名</div>
|
||
<div class="value">${escHtml(doc.office_name || "−")}</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="label">事業所番号 / 整理記号</div>
|
||
<div class="value">${escHtml(doc.office_number || "")} / ${escHtml(doc.office_kigo || "")}</div>
|
||
</div>
|
||
<div class="info-card">
|
||
<div class="label">担当年金事務所</div>
|
||
<div class="value">${escHtml(doc.nenkin_jimusho || "−")}</div>
|
||
</div>
|
||
${
|
||
doc.issue_date
|
||
? `<div class="info-card">
|
||
<div class="label">発行日</div>
|
||
<div class="value">${doc.issue_date.replace(/-/g, "/")}</div>
|
||
</div>`
|
||
: ""
|
||
}
|
||
</div>`;
|
||
|
||
let typeHtml = "";
|
||
if (doc.document_type === "SHAKAI") {
|
||
typeHtml = renderShakai(detail);
|
||
} else if (doc.document_type === "NOUNYU") {
|
||
typeHtml = renderNounyu(detail);
|
||
} else if (doc.document_type === "SHOYO") {
|
||
typeHtml = renderShoyo(detail);
|
||
}
|
||
|
||
body.innerHTML = infoHtml + typeHtml;
|
||
panel.classList.add("visible");
|
||
panel.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// 社会保険料額情報 (SHAKAI)
|
||
// ──────────────────────────────────
|
||
function renderShakai(d) {
|
||
const kigen = d.nofu_kigen_date
|
||
? d.nofu_kigen_date.replace(/-/g, "/")
|
||
: "−";
|
||
const addr = [
|
||
d.office_postcode ? `〒${d.office_postcode}` : "",
|
||
d.office_address,
|
||
]
|
||
.filter(Boolean)
|
||
.join(" ");
|
||
return `
|
||
<div class="amount-section">
|
||
<h3>保険料額</h3>
|
||
<div class="amount-grid">
|
||
${amtRow("健康保険料", d.kenkou_hokenryo)}
|
||
${amtRow("厚生年金保険料", d.kousei_nenkin_hokenryo)}
|
||
${amtRow("子ども・子育て拠出金", d.kodomo_kyoshutsukin)}
|
||
${amtRow("合 計", d.total, true)}
|
||
</div>
|
||
</div>
|
||
<div class="info-cards">
|
||
<div class="info-card">
|
||
<div class="label">納付期限</div>
|
||
<div class="value">${kigen}</div>
|
||
</div>
|
||
${
|
||
addr
|
||
? `<div class="info-card" style="flex:2">
|
||
<div class="label">事業所所在地</div>
|
||
<div class="value">${escHtml(addr)}</div>
|
||
</div>`
|
||
: ""
|
||
}
|
||
</div>`;
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// 保険料納入告知額・領収済額 (NOUNYU)
|
||
// ──────────────────────────────────
|
||
function renderNounyu(d) {
|
||
const kigen = d.kokuchi_nofu_kigen
|
||
? d.kokuchi_nofu_kigen.replace(/-/g, "/")
|
||
: "−";
|
||
const ryoshuDt = d.ryoshu_date ? d.ryoshu_date.replace(/-/g, "/") : "−";
|
||
const ryoshuPeriod =
|
||
d.ryoshu_nofu_year && d.ryoshu_nofu_month
|
||
? `${d.ryoshu_nofu_year}年${d.ryoshu_nofu_month}月分`
|
||
: "−";
|
||
return `
|
||
<div class="nounyu-compare">
|
||
<div class="nounyu-box kokuchi">
|
||
<h4>📋 今月の保険料告知額(納付期限: ${kigen})</h4>
|
||
<div class="amount-grid">
|
||
${amtRow("健康保険料", d.kokuchi_kenkou)}
|
||
${amtRow("厚生年金保険料", d.kokuchi_kounen)}
|
||
${amtRow("子ども・子育て拠出金", d.kokuchi_kodomo)}
|
||
${amtRow("合 計", d.kokuchi_total, true)}
|
||
</div>
|
||
</div>
|
||
<div class="nounyu-box ryoshu">
|
||
<h4>✅ 前月分領収済額(${ryoshuPeriod}・領収日: ${ryoshuDt})</h4>
|
||
<div class="amount-grid">
|
||
${amtRow("健康保険料", d.ryoshu_kenkou)}
|
||
${amtRow("厚生年金保険料", d.ryoshu_kounen)}
|
||
${amtRow("子ども・子育て拠出金", d.ryoshu_kodomo)}
|
||
${amtRow("合 計", d.ryoshu_total, true)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="info-cards">
|
||
<div class="info-card">
|
||
<div class="label">事業所所在地</div>
|
||
<div class="value">${escHtml([d.office_postcode ? "〒" + d.office_postcode : "", d.office_address].filter(Boolean).join(" "))}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// 賞与保険料算出内訳書 (SHOYO)
|
||
// ──────────────────────────────────
|
||
function renderShoyo(d) {
|
||
if (!d || !d.items) return "<p>詳細データなし</p>";
|
||
|
||
const totKenpo = sum(d.items, "kenpo_hongetsu");
|
||
const totKounen = sum(d.items, "kounen_hongetsu");
|
||
const totHKenpo = sum(d.items, "hyojun_shoyo_kenpo");
|
||
const totHKounen = sum(d.items, "hyojun_shoyo_kounen");
|
||
|
||
const rows = d.items
|
||
.map((it) => {
|
||
const hassei = it.hassei_date_iso
|
||
? it.hassei_date_iso.replace(/-/g, "/")
|
||
: it.hassei_ymd || "−";
|
||
return `<tr>
|
||
<td>${escHtml(it.seiri_num || "")}</td>
|
||
<td class="name">${escHtml(it.shimei || "")}</td>
|
||
<td>${hassei}</td>
|
||
<td>${fmtAmt(it.hyojun_shoyo_kenpo)}</td>
|
||
<td>${fmtAmt(it.hyojun_shoyo_kounen)}</td>
|
||
<td>${fmtAmt(it.kenpo_hongetsu)}</td>
|
||
<td>${it.kenpo_zengetsu ? fmtAmt(it.kenpo_zengetsu) : '<span class="empty-cell">−</span>'}</td>
|
||
<td>${fmtAmt(it.kounen_hongetsu)}</td>
|
||
<td>${it.kounen_zengetsu ? fmtAmt(it.kounen_zengetsu) : '<span class="empty-cell">−</span>'}</td>
|
||
</tr>`;
|
||
})
|
||
.join("");
|
||
|
||
return `
|
||
<div class="info-cards" style="margin-bottom:16px">
|
||
<div class="info-card">
|
||
<div class="label">対象人員</div>
|
||
<div class="value">${d.jin_in_num} 名</div>
|
||
</div>
|
||
${
|
||
d.menjyo_hokenryo_ritsu
|
||
? `<div class="info-card">
|
||
<div class="label">免除保険料率</div>
|
||
<div class="value">${escHtml(d.menjyo_hokenryo_ritsu)}</div>
|
||
</div>`
|
||
: ""
|
||
}
|
||
</div>
|
||
<div class="shoyo-table-wrap">
|
||
<table class="shoyo-table">
|
||
<thead>
|
||
<tr>
|
||
<th rowspan="2">#</th>
|
||
<th rowspan="2">氏名</th>
|
||
<th rowspan="2">賞与支払日</th>
|
||
<th colspan="2" style="background:#fff8e1">標準賞与額</th>
|
||
<th colspan="2" style="background:#e3f2fd">健康保険料</th>
|
||
<th colspan="2" style="background:#e8f5e9">厚生年金保険料</th>
|
||
</tr>
|
||
<tr>
|
||
<th style="background:#fff8e1">健保</th>
|
||
<th style="background:#fff8e1">厚年</th>
|
||
<th style="background:#e3f2fd">本月額</th>
|
||
<th style="background:#e3f2fd">前月以前</th>
|
||
<th style="background:#e8f5e9">本月額</th>
|
||
<th style="background:#e8f5e9">前月以前</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td colspan="3" style="text-align:center">合 計</td>
|
||
<td>${fmtAmt(totHKenpo)}</td>
|
||
<td>${fmtAmt(totHKounen)}</td>
|
||
<td>${fmtAmt(totKenpo)}</td>
|
||
<td>−</td>
|
||
<td>${fmtAmt(totKounen)}</td>
|
||
<td>−</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>`;
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// アップロードゾーン
|
||
// ──────────────────────────────────
|
||
function initUploadZone() {
|
||
const zone = document.getElementById("upload-zone");
|
||
const input = document.getElementById("zip-input");
|
||
|
||
zone.addEventListener("dragover", (e) => {
|
||
e.preventDefault();
|
||
zone.classList.add("dragover");
|
||
});
|
||
zone.addEventListener("dragleave", () =>
|
||
zone.classList.remove("dragover"),
|
||
);
|
||
zone.addEventListener("drop", (e) => {
|
||
e.preventDefault();
|
||
zone.classList.remove("dragover");
|
||
const files = [...e.dataTransfer.files].filter((f) =>
|
||
f.name.endsWith(".zip"),
|
||
);
|
||
if (files.length) uploadFiles(files);
|
||
});
|
||
|
||
input.addEventListener("change", () => {
|
||
const files = [...input.files];
|
||
if (files.length) uploadFiles(files);
|
||
input.value = "";
|
||
});
|
||
}
|
||
|
||
async function uploadFiles(files) {
|
||
const status = document.getElementById("upload-status");
|
||
status.innerHTML = `<span style="color:#6c757d">アップロード中... (${files.length}件)</span>`;
|
||
|
||
let ok = 0,
|
||
skipped = 0,
|
||
errors = [];
|
||
|
||
for (const file of files) {
|
||
const fd = new FormData();
|
||
fd.append("file", file);
|
||
try {
|
||
const res = await fetch(`${API}/nenkin/upload`, {
|
||
method: "POST",
|
||
body: fd,
|
||
});
|
||
const json = await res.json();
|
||
if (res.status === 409) {
|
||
skipped++;
|
||
} else if (!res.ok) {
|
||
errors.push(`${file.name}: ${json.detail || res.statusText}`);
|
||
} else {
|
||
ok++;
|
||
}
|
||
} catch (e) {
|
||
errors.push(`${file.name}: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
let msg = "";
|
||
if (ok > 0) msg += `<span class="status-ok">✓ ${ok}件登録完了</span> `;
|
||
if (skipped > 0)
|
||
msg += `<span style="color:#856404">⚠ ${skipped}件は既登録のためスキップ</span> `;
|
||
if (errors.length)
|
||
msg += `<span class="status-err">✗ エラー: ${errors.join(" / ")}</span>`;
|
||
status.innerHTML = msg;
|
||
|
||
if (ok > 0) {
|
||
loadDocuments();
|
||
loadFilterOptions();
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────
|
||
// ユーティリティ
|
||
// ──────────────────────────────────
|
||
function escHtml(s) {
|
||
return String(s)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function fmtAmt(v) {
|
||
if (v == null || v === "") return '<span class="empty-cell">−</span>';
|
||
return (
|
||
"¥" +
|
||
Number(v).toLocaleString("ja-JP", {
|
||
minimumFractionDigits: 0,
|
||
maximumFractionDigits: 0,
|
||
})
|
||
);
|
||
}
|
||
|
||
function amtRow(label, value, isTotal = false) {
|
||
return `<div class="amount-row${isTotal ? " total" : ""}">
|
||
<span>${label}</span>
|
||
<span class="amt">${fmtAmt(value)}</span>
|
||
</div>`;
|
||
}
|
||
|
||
function sum(items, key) {
|
||
return items.reduce((acc, it) => acc + (Number(it[key]) || 0), 0);
|
||
}
|
||
|
||
function showDetailError(msg) {
|
||
const panel = document.getElementById("detail-panel");
|
||
document.getElementById("detail-body").innerHTML =
|
||
`<p style="color:#dc3545">エラー: ${escHtml(msg)}</p>`;
|
||
panel.classList.add("visible");
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|