修正
This commit is contained in:
@@ -119,6 +119,16 @@
|
||||
<p><a class="btn" href="payroll.html">給与システムを開く</a></p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>📋 社会保険電子文書</h2>
|
||||
<p>
|
||||
日本年金機構からダウンロードした ZIP ファイルを読み込み、社会保険料額・納入告知・賞与算出内訳などを確認できます。
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn" href="nenkin-portal.html" style="background: #17a2b8">文書ポータルを開く</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>🏛 e-Gov 電子送達</h2>
|
||||
<p>
|
||||
|
||||
735
frontend/nenkin-portal.html
Normal file
735
frontend/nenkin-portal.html
Normal file
@@ -0,0 +1,735 @@
|
||||
<!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" />
|
||||
<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>
|
||||
@@ -119,6 +119,15 @@
|
||||
<h2>源泉徴収票</h2>
|
||||
<p>給与所得の源泉徴収票を発行します(本人用・区役所用・税務署用 各1部)</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="menu-card"
|
||||
onclick="location.href = 'nenkin-portal.html'"
|
||||
>
|
||||
<div class="icon">📋</div>
|
||||
<h2>社会保険電子文書</h2>
|
||||
<p>日本年金機構からの電子文書(保険料通知・賞与内訳等)を管理します</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user