This commit is contained in:
admin
2026-05-22 23:05:38 +09:00
parent e8690d130b
commit ebc87f96f9
6 changed files with 1783 additions and 1214 deletions

View File

@@ -10,7 +10,6 @@ html {
} }
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
/* ── body 余白を縮小 ── */ /* ── body 余白を縮小 ── */
body { body {
padding: 12px !important; padding: 12px !important;
@@ -18,8 +17,12 @@ html {
} }
/* ── h1・h2 フォントサイズ ── */ /* ── h1・h2 フォントサイズ ── */
h1 { font-size: 1.35rem !important; } h1 {
h2 { font-size: 1.1rem !important; } font-size: 1.35rem !important;
}
h2 {
font-size: 1.1rem !important;
}
/* ================================================================ /* ================================================================
テーブル: 横スクロール テーブル: 横スクロール
@@ -320,5 +323,4 @@ html {
width: 100% !important; width: 100% !important;
box-sizing: border-box !important; box-sizing: border-box !important;
} }
} }

View File

@@ -863,7 +863,19 @@
<div class="search-buttons"> <div class="search-buttons">
<button onclick="searchJournals()">🔍 検索</button> <button onclick="searchJournals()">🔍 検索</button>
<button onclick="printSearchResults()">🖨️ 印刷</button> <button onclick="printSearchResults()">🖨️ 印刷</button>
<button onclick="clearSearchConditions()" style="background: #6c757d; color: white; border: none; padding: 6px 14px; border-radius: 4px; cursor: pointer;">🔄 条件クリア</button> <button
onclick="clearSearchConditions()"
style="
background: #6c757d;
color: white;
border: none;
padding: 6px 14px;
border-radius: 4px;
cursor: pointer;
"
>
🔄 条件クリア
</button>
</div> </div>
</div> </div>
@@ -2830,7 +2842,8 @@
// 複数行フォームへのコピー // 複数行フォームへのコピー
function copyJournalToDetailForm(data) { function copyJournalToDetailForm(data) {
document.getElementById("detailEntryDate").value = data.entry_date; document.getElementById("detailEntryDate").value = data.entry_date;
document.getElementById("detailDescription").value = data.description || ""; document.getElementById("detailDescription").value =
data.description || "";
// 既存行をクリア // 既存行をクリア
document.getElementById("detailLinesTbody").innerHTML = ""; document.getElementById("detailLinesTbody").innerHTML = "";
@@ -2846,10 +2859,14 @@
accountInput.dataset.accountId = acc.account_id; accountInput.dataset.accountId = acc.account_id;
} }
if (Number(line.debit) > 0) { if (Number(line.debit) > 0) {
tr.querySelector(".detail-debit").value = Number(line.debit).toLocaleString(); tr.querySelector(".detail-debit").value = Number(
line.debit,
).toLocaleString();
} }
if (Number(line.credit) > 0) { if (Number(line.credit) > 0) {
tr.querySelector(".detail-credit").value = Number(line.credit).toLocaleString(); tr.querySelector(".detail-credit").value = Number(
line.credit,
).toLocaleString();
} }
if (line.line_description) { if (line.line_description) {
tr.querySelector(".detail-line-memo").value = line.line_description; tr.querySelector(".detail-line-memo").value = line.line_description;
@@ -2927,16 +2944,14 @@
if (kamokuAcc) { if (kamokuAcc) {
document.getElementById("entryAccountInput").value = document.getElementById("entryAccountInput").value =
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`; `${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
document.getElementById( document.getElementById("entryAccountInput").dataset.accountId =
"entryAccountInput", kamokuAcc.account_id;
).dataset.accountId = kamokuAcc.account_id;
} }
if (methodAcc) { if (methodAcc) {
document.getElementById("entryMethodInput").value = document.getElementById("entryMethodInput").value =
`${methodAcc.account_code} ${methodAcc.account_name}`; `${methodAcc.account_code} ${methodAcc.account_name}`;
document.getElementById( document.getElementById("entryMethodInput").dataset.accountId =
"entryMethodInput", methodAcc.account_id;
).dataset.accountId = methodAcc.account_id;
} }
const totalAmount = Number(mainCredit.credit); const totalAmount = Number(mainCredit.credit);
document.getElementById("entryAmount").value = document.getElementById("entryAmount").value =
@@ -2958,16 +2973,14 @@
if (kamokuAcc) { if (kamokuAcc) {
document.getElementById("entryAccountInput").value = document.getElementById("entryAccountInput").value =
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`; `${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
document.getElementById( document.getElementById("entryAccountInput").dataset.accountId =
"entryAccountInput", kamokuAcc.account_id;
).dataset.accountId = kamokuAcc.account_id;
} }
if (methodAcc) { if (methodAcc) {
document.getElementById("entryMethodInput").value = document.getElementById("entryMethodInput").value =
`${methodAcc.account_code} ${methodAcc.account_name}`; `${methodAcc.account_code} ${methodAcc.account_name}`;
document.getElementById( document.getElementById("entryMethodInput").dataset.accountId =
"entryMethodInput", methodAcc.account_id;
).dataset.accountId = methodAcc.account_id;
} }
const totalAmount = Number(mainDebit.debit); const totalAmount = Number(mainDebit.debit);
document.getElementById("entryAmount").value = document.getElementById("entryAmount").value =
@@ -2993,9 +3006,8 @@
).checked = true; ).checked = true;
document.getElementById("entryAccountInput").value = document.getElementById("entryAccountInput").value =
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`; `${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
document.getElementById( document.getElementById("entryAccountInput").dataset.accountId =
"entryAccountInput", kamokuAccD.account_id;
).dataset.accountId = kamokuAccD.account_id;
const methodAcc = accounts.find( const methodAcc = accounts.find(
(a) => a.account_id === mainCredit.account_id, (a) => a.account_id === mainCredit.account_id,
); );
@@ -3025,9 +3037,8 @@
} }
document.getElementById("entryMethodInput").value = document.getElementById("entryMethodInput").value =
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`; `${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
document.getElementById( document.getElementById("entryMethodInput").dataset.accountId =
"entryMethodInput", kamokuAccD.account_id;
).dataset.accountId = kamokuAccD.account_id;
document.getElementById("entryAmount").value = Number( document.getElementById("entryAmount").value = Number(
mainDebit.debit, mainDebit.debit,
).toLocaleString(); ).toLocaleString();

View File

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

View File

@@ -1783,14 +1783,18 @@
// 源泉税控除対象人数を計算(給与と同じロジック) // 源泉税控除対象人数を計算(給与と同じロジック)
const bDependentCount = employee.dependents const bDependentCount = employee.dependents
? employee.dependents.filter((d) => { ? employee.dependents.filter((d) => {
if (d.valid_to && new Date(d.valid_to) < new Date()) return false; if (d.valid_to && new Date(d.valid_to) < new Date())
return false;
if (!d.birth_date) return false; if (!d.birth_date) return false;
const birthDate = new Date(d.birth_date); const birthDate = new Date(d.birth_date);
const targetYear = bonus.bonus_year; const targetYear = bonus.bonus_year;
const yearEnd = new Date(targetYear, 11, 31); const yearEnd = new Date(targetYear, 11, 31);
let age = yearEnd.getFullYear() - birthDate.getFullYear(); let age = yearEnd.getFullYear() - birthDate.getFullYear();
const monthDiff = yearEnd.getMonth() - birthDate.getMonth(); const monthDiff = yearEnd.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())) { if (
monthDiff < 0 ||
(monthDiff === 0 && yearEnd.getDate() < birthDate.getDate())
) {
age--; age--;
} }
return age >= 16; return age >= 16;
@@ -1969,10 +1973,10 @@
<div class="form-group"> <div class="form-group">
<label>賞与種別</label> <label>賞与種別</label>
<select name="bonus_type"> <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>
<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> </select>
</div> </div>
</div> </div>
@@ -2451,7 +2455,7 @@
const bonusPaymentDate = bonus.payment_date const bonusPaymentDate = bonus.payment_date
? (() => { ? (() => {
const d = new Date(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')}`; 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)}`; : `${bonus.bonus_year}${String(bonus.bonus_month).padStart(2, "0")}${getLastDayOfMonth(bonus.bonus_year, bonus.bonus_month)}`;
if (currentKey && currentKey !== currentKey_new) { if (currentKey && currentKey !== currentKey_new) {

View File

@@ -118,16 +118,17 @@
> >
<div class="icon">📄</div> <div class="icon">📄</div>
<h2>源泉徴収票</h2> <h2>源泉徴収票</h2>
<p>給与所得の源泉徴収票を発行します(本人用・区役所用・税務署用 各1部</p> <p>
給与所得の源泉徴収票を発行します(本人用・区役所用・税務署用 各1部
</p>
</div> </div>
<div <div class="menu-card" onclick="location.href = 'nenkin-portal.html'">
class="menu-card"
onclick="location.href = 'nenkin-portal.html'"
>
<div class="icon">📋</div> <div class="icon">📋</div>
<h2>社会保険電子文書</h2> <h2>社会保険電子文書</h2>
<p>日本年金機構からの電子文書(保険料通知・賞与内訳等)を管理します</p> <p>
日本年金機構からの電子文書(保険料通知・賞与内訳等)を管理します
</p>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff