Files
2026-06-07 21:58:20 +09:00

1104 lines
34 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>e-Gov 電子送達 NJTS</title>
<script src="js/auth.js"></script>
<link rel="stylesheet" href="/css/mobile.css" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
"Yu Gothic UI",
"メイリオ",
sans-serif;
background: #f5f6fa;
padding: 24px;
color: #212529;
}
header {
max-width: 660px;
margin: 0 auto 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 18px;
}
.back-link {
font-size: 13px;
color: #0d6efd;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
.card {
max-width: 660px;
margin: 0 auto 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 28px 32px;
}
.card h2 {
font-size: 16px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 8px;
}
.card .desc {
font-size: 13px;
color: #6c757d;
margin-bottom: 20px;
}
/* ステータスバー */
.status-bar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 6px;
background: #f8f9fa;
margin-bottom: 20px;
font-size: 13px;
}
.badge {
padding: 3px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.idle {
background: #e9ecef;
color: #6c757d;
}
.starting {
background: #fff3cd;
color: #856404;
}
.waiting_mfa {
background: #cff4fc;
color: #055160;
}
.logged_in {
background: #d1e7dd;
color: #0a3622;
}
.downloading {
background: #cfe2ff;
color: #084298;
}
.error {
background: #f8d7da;
color: #842029;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #dee2e6;
border-top-color: #0d6efd;
border-radius: 50%;
animation: spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* フォーム */
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
}
.form-group input {
width: 100%;
padding: 9px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
}
.form-group input:focus {
outline: none;
border-color: #86b7fe;
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
}
.btn {
padding: 10px 22px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-primary {
background: #6f42c1;
color: #fff;
}
.btn-primary:hover {
background: #5a32a3;
}
.btn-secondary {
background: #6c757d;
color: #fff;
}
.btn-secondary:hover {
background: #5c636a;
}
/* MFA */
.mfa-notice {
background: #e7f3ff;
border: 1px solid #b6d4fe;
border-radius: 6px;
padding: 12px 14px;
font-size: 13px;
margin-bottom: 16px;
line-height: 1.6;
}
.mfa-input {
font-size: 30px;
letter-spacing: 12px;
text-align: center;
width: 100%;
padding: 10px;
border: 2px solid #6f42c1;
border-radius: 5px;
}
.btn-success {
background: #198754;
color: #fff;
}
.btn-success:hover {
background: #146c43;
}
.btn-success:disabled {
background: #a3cfbb;
cursor: not-allowed;
}
/* ダウンロード結果 */
.dl-result {
margin-top: 14px;
padding: 12px 16px;
background: #d1e7dd;
border: 1px solid #a3cfbb;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: center;
gap: 12px;
}
.dl-result a {
margin-left: auto;
font-weight: 700;
color: #0a3622;
text-decoration: none;
background: #fff;
border: 1px solid #a3cfbb;
padding: 4px 12px;
border-radius: 4px;
}
.dl-result a:hover {
background: #c2e0cc;
}
.screenshot-wrap summary {
font-size: 12px;
color: #888;
cursor: pointer;
margin-bottom: 6px;
}
.screenshot-wrap img {
max-width: 100%;
border: 1px solid #dee2e6;
border-radius: 4px;
}
/* ログイン済みアクション */
.action-note {
font-size: 13px;
color: #555;
margin-bottom: 16px;
line-height: 1.6;
}
/* API 認証 */
.btn-gbiz {
background: #0077cc;
color: #fff;
width: 100%;
text-align: center;
padding: 12px 22px;
font-size: 15px;
}
.btn-gbiz:hover {
background: #0055a5;
}
.method-tabs {
display: flex;
gap: 6px;
margin-bottom: 18px;
}
.method-tab {
flex: 1;
padding: 8px;
border: 2px solid #dee2e6;
border-radius: 5px;
background: #f8f9fa;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-align: center;
color: #6c757d;
}
.method-tab.active {
border-color: #6f42c1;
background: #f3eeff;
color: #6f42c1;
}
/* テーブル */
.tbl-wrap {
overflow-x: auto;
margin-top: 12px;
}
table.apply-tbl {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
table.apply-tbl th,
table.apply-tbl td {
border: 1px solid #dee2e6;
padding: 5px 8px;
white-space: nowrap;
vertical-align: top;
}
table.apply-tbl th {
background: #f1f3f5;
font-weight: 700;
}
table.apply-tbl tr:hover td {
background: #f8f9fa;
}
.proc-select {
width: 100%;
padding: 8px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 13px;
margin-bottom: 10px;
}
.arrive-badge {
font-weight: 700;
color: #0a3622;
background: #d1e7dd;
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
cursor: pointer;
}
.info-box {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 6px;
padding: 10px 14px;
font-size: 12px;
line-height: 1.7;
margin-bottom: 14px;
}
.env-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 700;
}
.env-sandbox {
background: #fff3cd;
color: #856404;
}
.env-production {
background: #d1e7dd;
color: #0a3622;
}
</style>
</head>
<body>
<header>
<h1>🏛 e-Gov 電子送達</h1>
<a
class="back-link"
href="index.html"
style="
display: inline-block;
padding: 6px 16px;
background: #757575;
color: #fff;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
"
>← メニューへ戻る</a
>
</header>
<!-- ログインカード -->
<div class="card">
<h2>🔐 e-Gov ログインGビズID</h2>
<p class="desc">GビズID プレミアムで e-Gov にログインします。</p>
<!-- ステータス -->
<div class="status-bar">
<span class="badge idle" id="badge">未ログイン</span>
<span class="spinner" id="spinner" style="display: none"></span>
<span id="msgText" style="color: #555"></span>
</div>
<!-- 認証方式タブ -->
<div class="method-tabs" id="methodTabs">
<div class="method-tab active" id="tabApi" onclick="switchTab('api')">
🔑 API認証推奨
</div>
<div class="method-tab" id="tabBrowser" onclick="switchTab('browser')">
🌐 ブラウザ自動操作
</div>
</div>
<!-- ─── API 認証セクション ─── -->
<div id="apiAuthSection">
<p
style="
font-size: 13px;
color: #555;
margin-bottom: 16px;
line-height: 1.6;
"
>
公式 e-Gov API を使用してGビズIDで認証します。<br />
ボタンを押すとGビズIDのログインページに移動します。認証後、自動でここに戻ります。
</p>
<button class="btn btn-gbiz" onclick="startApiAuth()">
GビズID でログインAPI方式
</button>
</div>
<!-- ─── Playwright セクション ─── -->
<div id="loginSection" style="display: none">
<div class="form-group">
<label>GビズIDメールアドレス</label>
<input
type="email"
id="userId"
placeholder="info@company.co.jp"
autocomplete="username"
/>
</div>
<div class="form-group">
<label>パスワード</label>
<input
type="password"
id="password"
autocomplete="current-password"
onkeydown="if (event.key === 'Enter') startLogin();"
/>
</div>
<button class="btn btn-primary" onclick="startLogin()">
ログイン開始
</button>
</div>
<!-- ② MFA入力WAITING_MFA 時のみ表示) -->
<div id="mfaSection" style="display: none">
<div class="mfa-notice">
📱 <strong>GビズID アプリ</strong>を開き、表示されている
<strong>6桁の認証コード</strong> を入力してください。<br />
<small
>※ コードは 30
秒ごとに更新されます。新しいコードで入力してください。</small
>
</div>
<div class="form-group">
<label>認証コード6桁</label>
<input
class="mfa-input"
type="text"
id="mfaCode"
maxlength="6"
inputmode="numeric"
pattern="[0-9]*"
placeholder="000000"
oninput="if (this.value.length === 6) submitMfa();"
/>
</div>
<button class="btn btn-primary" onclick="submitMfa()">認証</button>
</div>
<!-- ③ ログイン済み -->
<div id="loggedSection" style="display: none">
<p class="action-note">
✅ e-Gov にログインしました。<br />
電子送達一覧から最初のファイルをダウンロードできます。
</p>
<div
style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center"
>
<button class="btn btn-success" id="dlBtn" onclick="startDownload()">
📥 電子送達をダウンロード
</button>
<button class="btn btn-secondary" onclick="doLogout()">
ログアウト
</button>
</div>
<div id="dlResult" style="display: none" class="dl-result">
<span id="dlMsg"></span>
<a id="dlLink" href="/egov/downloads/latest" target="_blank">保存</a>
</div>
</div>
<!-- スクリーンショット(デバッグ) -->
<div class="screenshot-wrap" id="ssWrap" style="display: none">
<details>
<summary>ブラウザ画面を確認(デバッグ用)</summary>
<img id="ssImg" src="" alt="スクリーンショット" />
</details>
</div>
</div>
<!-- ─── 申請書作成(最終確認試験用) ───────────────────── -->
<div class="card" id="applyCard">
<h2>📝 テスト申請書作成(最終確認試験用)</h2>
<p class="desc">
検証環境へテスト手続を送信して到達番号を取得します。<br />
取得した到達番号は試験データ表と Excel
に記入してデジタル庁へ送付してください。
</p>
<div class="info-box">
⚠️ ログイン後に操作可能です。<br />
環境:
<span id="envLabel" class="env-badge env-sandbox">sandbox検証</span>
&nbsp; ソフトウェアID:
<code id="swIdLabel" style="font-size: 11px"></code>
</div>
<div class="form-group">
<label>手続識別子(対象手続を選択)</label>
<select class="proc-select" id="procSelect">
<optgroup label="電子送達関係">
<option value="900A013800001000">
900A013800001000 APIテスト用手続電子送達0001電子送達1件
</option>
</optgroup>
<optgroup label="社会保険関係 標準形式">
<option value="950A010002010000">
950A010002010000 APIテスト社保通1007 公文書(署名あり)
</option>
<option value="950A010002011000">
950A010002011000 APIテスト社保通1008 公文書(署名なし)
</option>
<option value="950A010700001000">
950A010700001000 APIテスト社保通1001 手続終了
</option>
<option value="950A010700002000">
950A010700002000 APIテスト社保通1002 手続終了
</option>
<option value="950A010700005000">
950A010700005000 APIテスト社保通1003 取下げ承認
</option>
<option value="950A010700006000">
950A010700006000 APIテスト社保通1004 取下げ却下
</option>
</optgroup>
<optgroup label="労働保険関係 標準形式">
<option value="950A010200003000">
950A010200003000 APIテスト労保通1001 手続終了
</option>
<option value="950A010200004000">
950A010200004000 APIテスト労保通1002 手続終了
</option>
<option value="950A010002012000">
950A010002012000 APIテスト労保通1004 コメント(メッセージ)
</option>
<option value="950A010002016000">
950A010002016000 APIテスト労保通1008 コメント(公文書)
</option>
</optgroup>
<optgroup label="社会保険関係 個別署名形式">
<option value="950A101220029000">
950A101220029000 APIテスト社保個1001 手続終了
</option>
<option value="950A102200038000">
950A102200038000 APIテスト社保個1005 コメント(メッセージ)
</option>
<option value="950A102200039000">
950A102200039000 APIテスト社保個1006 公文書(署名あり)
</option>
</optgroup>
<optgroup label="その他">
<option value="900A020700013000">
900A020700013000 APIテスト労保適用0003 補正申請
</option>
<option value="900A102800072000">
900A102800072000 APIテスト労保適用個0014 プレ印字
</option>
</optgroup>
</select>
</div>
<div
style="
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 14px;
"
>
<button
class="btn btn-primary"
id="applyBtn"
onclick="doTestApply()"
style="background: #6f42c1"
>
📤 テスト申請を送信
</button>
<span
id="applySpinner"
style="display: none; font-size: 13px; color: #555"
>⏳ 送信中...</span
>
</div>
<!-- 結果テーブル -->
<div id="applyResult" style="display: none">
<div class="dl-result" style="margin-bottom: 10px">
<span>✅ 到達番号: </span>
<strong
id="arriveIdResult"
class="arrive-badge"
title="クリックでコピー"
onclick="copyArriveId()"
></strong>
<span
style="font-size: 12px; color: #6c757d; margin-left: 8px"
id="arriveDateResult"
></span>
</div>
<div style="font-size: 12px; color: #555" id="procNameResult"></div>
</div>
<div
id="applyError"
style="
display: none;
color: #842029;
font-size: 13px;
margin-top: 8px;
padding: 8px 12px;
background: #f8d7da;
border-radius: 4px;
"
></div>
</div>
<!-- ─── 申請案件一覧 ─────────────────────────────────────── -->
<div class="card" id="listCard">
<h2>📋 申請案件一覧(到達番号確認)</h2>
<p class="desc">
送信済みの申請案件一覧を表示します。到達番号を Excel
に転記してデジタル庁へ提出してください。
</p>
<div
style="display: flex; gap: 10px; margin-bottom: 12px; flex-wrap: wrap"
>
<button class="btn btn-secondary" onclick="loadApplyList()">
🔄 一覧を更新
</button>
<button
class="btn"
onclick="exportApplyListCsv()"
style="background: #0d6efd; color: #fff"
>
📥 CSV エクスポート
</button>
</div>
<div
id="listStatus"
style="font-size: 13px; color: #6c757d; margin-bottom: 8px"
></div>
<div class="tbl-wrap" id="applyListWrap">
<table class="apply-tbl">
<thead>
<tr>
<th>#</th>
<th>到達番号</th>
<th>到達日時</th>
<th>手続識別子</th>
<th>手続名</th>
<th>ステータス</th>
<th>サブステータス</th>
</tr>
</thead>
<tbody id="applyListBody">
<tr>
<td
colspan="7"
style="text-align: center; color: #aaa; padding: 20px"
>
「一覧を更新」ボタンで取得してください
</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
let timer = null;
let currentTab = "api";
// ─── タブ切り替え ──────────────────────────────────────
function switchTab(tab) {
currentTab = tab;
document
.getElementById("tabApi")
.classList.toggle("active", tab === "api");
document
.getElementById("tabBrowser")
.classList.toggle("active", tab === "browser");
if (tab === "api") {
show("apiAuthSection");
hide("loginSection");
} else {
hide("apiAuthSection");
show("loginSection");
}
}
// ─── API 認証 ──────────────────────────────────────────
async function startApiAuth() {
const res = await fetch("/egov/auth-url");
if (!res.ok) {
const e = await res.json();
alert(
e.detail ||
"認証URLの取得に失敗しました。\n/egov/config で資格情報を設定してください。",
);
return;
}
const { auth_url } = await res.json();
// 同じタブで遷移(リダイレクトバック受信のため)
location.href = auth_url;
}
// ─── OAuth2 コールバック処理 ───────────────────────────
async function handleOAuthCallback(code, oauthState) {
setUI("starting", "e-Gov 認証コードを処理中...");
hide("apiAuthSection");
hide("loginSection");
hide("methodTabs");
try {
const res = await fetch("/egov/callback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code, state: oauthState }),
});
// URL からパラメータを削除
history.replaceState({}, "", "/egov.html");
if (res.ok) {
setUI("logged_in", "API認証完了");
show("loggedSection");
} else {
const e = await res.json();
setUI("error", "API認証エラー: " + (e.detail || "不明なエラー"));
show("methodTabs");
show("apiAuthSection");
}
} catch (err) {
history.replaceState({}, "", "/egov.html");
setUI("error", "コールバックエラー: " + err.message);
show("methodTabs");
show("apiAuthSection");
}
}
async function startLogin() {
const userId = document.getElementById("userId").value.trim();
const password = document.getElementById("password").value;
if (!userId || !password) {
alert("GビズID とパスワードを入力してください");
return;
}
const res = await fetch("/egov/login-start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_id: userId, password }),
});
if (!res.ok) {
const e = await res.json();
alert(e.detail || "エラー");
return;
}
startPolling();
}
async function submitMfa() {
const code = document.getElementById("mfaCode").value.trim();
if (code.length !== 6) {
alert("6桁のコードを入力してください");
return;
}
const res = await fetch("/egov/login-mfa", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mfa_code: code }),
});
if (!res.ok) {
const e = await res.json();
alert(e.detail || "エラー");
return;
}
hide("mfaSection");
setUI("starting", "認証コードを送信中...");
}
async function startDownload() {
document.getElementById("dlBtn").disabled = true;
document.getElementById("dlBtn").textContent = "⏳ ダウンロード中...";
hide("dlResult");
const res = await fetch("/egov/download-start", { method: "POST" });
if (!res.ok) {
const e = await res.json();
alert(e.detail || "ダウンロード開始エラー");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
return;
}
startPolling();
}
async function doLogout() {
await fetch("/egov/logout", { method: "POST" });
stopPolling();
hide("loggedSection");
show("loginSection");
document.getElementById("mfaCode").value = "";
hide("dlResult");
setUI("idle", "");
}
function startPolling() {
if (timer) return;
timer = setInterval(async () => {
try {
const r = await fetch("/egov/status");
apply(await r.json());
} catch (_) {}
}, 1500);
}
function stopPolling() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function apply(d) {
setUI(d.status, d.message);
if (d.screenshot) {
document.getElementById("ssImg").src =
"data:image/jpeg;base64," + d.screenshot;
show("ssWrap");
}
// API トークンが有効 or ログイン済み
if (d.token_valid || d.status === "logged_in") {
hide("methodTabs");
hide("apiAuthSection");
hide("loginSection");
hide("mfaSection");
show("loggedSection");
if (d.last_download) {
const dlMsg = document.getElementById("dlMsg");
dlMsg.textContent = `${d.last_download.filename} (${(d.last_download.size / 1024).toFixed(1)} KB)`;
show("dlResult");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
stopPolling();
} else if (
d.status === "logged_in" &&
!["downloading"].includes(d.status)
) {
stopPolling();
}
return;
}
if (d.status === "waiting_mfa") {
hide("loginSection");
hide("apiAuthSection");
show("mfaSection");
setTimeout(() => document.getElementById("mfaCode").focus(), 100);
} else if (d.status === "error") {
stopPolling();
if (currentTab === "api") {
show("apiAuthSection");
hide("loginSection");
} else {
show("loginSection");
hide("apiAuthSection");
}
hide("mfaSection");
hide("loggedSection");
document.getElementById("dlBtn").disabled = false;
document.getElementById("dlBtn").textContent =
"📥 電子送達をダウンロード";
}
}
function setUI(status, msg) {
const labels = {
idle: "未ログイン",
starting: "ログイン中",
waiting_mfa: "認証待ち",
logged_in: "ログイン済み",
downloading: "DL中",
error: "エラー",
};
const badge = document.getElementById("badge");
badge.textContent = labels[status] || status;
badge.className = "badge " + status;
document.getElementById("msgText").textContent = msg || "";
document.getElementById("spinner").style.display = [
"starting",
"waiting_mfa",
"downloading",
].includes(status)
? "inline-block"
: "none";
}
function show(id) {
document.getElementById(id).style.display = "";
}
function hide(id) {
document.getElementById(id).style.display = "none";
}
// 初期状態取得
(async () => {
// OAuth2 コールバック検出
const params = new URLSearchParams(location.search);
const code = params.get("code");
const oauthState = params.get("state");
if (code) {
await handleOAuthCallback(code, oauthState || "");
return;
}
// 通常ロード: ステータス確認
try {
const r = await fetch("/egov/status");
const d = await r.json();
apply(d);
// 環境・ソフトウェアID を表示
document.getElementById("envLabel").textContent =
d.environment === "sandbox"
? "sandbox検証環境"
: "production本番環境";
document.getElementById("envLabel").className =
"env-badge " +
(d.environment === "sandbox" ? "env-sandbox" : "env-production");
if (!["idle", "logged_in", "error"].includes(d.status))
startPolling();
} catch (_) {}
// ソフトウェアID 表示
try {
const r2 = await fetch("/egov/config-info");
if (r2.ok) {
const cfg = await r2.json();
document.getElementById("swIdLabel").textContent =
cfg.software_id || "—";
}
} catch (_) {}
})();
// ─── テスト申請書作成 ────────────────────────────────────
let _applyResults = [];
async function doTestApply() {
const procId = document.getElementById("procSelect").value;
const applyBtn = document.getElementById("applyBtn");
const spinner = document.getElementById("applySpinner");
const resultDiv = document.getElementById("applyResult");
const errorDiv = document.getElementById("applyError");
applyBtn.disabled = true;
show("applySpinner");
resultDiv.style.display = "none";
errorDiv.style.display = "none";
try {
const res = await fetch("/egov/test-apply", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ proc_id: procId }),
});
const data = await res.json();
if (!res.ok) {
errorDiv.textContent =
"エラー: " + (data.detail || JSON.stringify(data));
errorDiv.style.display = "block";
return;
}
document.getElementById("arriveIdResult").textContent =
data.arrive_id || "(取得中)";
document.getElementById("arriveDateResult").textContent =
data.arrive_date ? ` 到達: ${data.arrive_date}` : "";
document.getElementById("procNameResult").textContent =
data.proc_name || "";
resultDiv.style.display = "block";
// 送信済みリストに追加
_applyResults.unshift({
arrive_id: data.arrive_id,
arrive_date: data.arrive_date,
proc_id: procId,
proc_name: data.proc_name,
status: "",
sub_status: "",
});
renderApplyList(_applyResults);
} catch (err) {
errorDiv.textContent = "通信エラー: " + err.message;
errorDiv.style.display = "block";
} finally {
applyBtn.disabled = false;
hide("applySpinner");
}
}
function copyArriveId() {
const val = document.getElementById("arriveIdResult").textContent;
navigator.clipboard
.writeText(val)
.then(() => alert("コピーしました: " + val))
.catch(() => {});
}
// ─── 申請案件一覧 ────────────────────────────────────────
async function loadApplyList() {
const statusDiv = document.getElementById("listStatus");
statusDiv.textContent = "⏳ 一覧を取得中...";
try {
const res = await fetch("/egov/apply-list");
if (!res.ok) {
const e = await res.json();
statusDiv.textContent = "エラー: " + (e.detail || "不明");
return;
}
const data = await res.json();
_applyResults = data.apply_list || [];
renderApplyList(_applyResults);
statusDiv.textContent = `${data.total}`;
} catch (err) {
statusDiv.textContent = "通信エラー: " + err.message;
}
}
function renderApplyList(items) {
const tbody = document.getElementById("applyListBody");
if (!items.length) {
tbody.innerHTML =
'<tr><td colspan="7" style="text-align:center;color:#aaa;padding:20px;">申請案件はありません</td></tr>';
return;
}
tbody.innerHTML = items
.map(
(it, i) => `
<tr>
<td>${i + 1}</td>
<td><span class="arrive-badge" onclick="navigator.clipboard.writeText('${esc(it.arrive_id)}')" title="クリックでコピー">${esc(it.arrive_id)}</span></td>
<td>${esc(it.arrive_date)}</td>
<td style="font-family:monospace;font-size:11px">${esc(it.proc_id)}</td>
<td>${esc(it.proc_name)}</td>
<td>${esc(it.status)}</td>
<td>${esc(it.sub_status)}</td>
</tr>
`,
)
.join("");
}
function esc(s) {
return String(s || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function exportApplyListCsv() {
if (!_applyResults.length) {
alert("データがありません");
return;
}
const header = [
"到達番号",
"到達日時",
"手続識別子",
"手続名",
"ステータス",
"サブステータス",
];
const rows = _applyResults.map((it) =>
[
it.arrive_id,
it.arrive_date,
it.proc_id,
it.proc_name,
it.status,
it.sub_status,
]
.map((v) => `"${String(v || "").replace(/"/g, '""')}"`)
.join(","),
);
const csv = "\uFEFF" + [header.join(","), ...rows].join("\r\n");
const link = document.createElement("a");
link.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
link.download = "egov_apply_list.csv";
link.click();
}
</script>
</body>
</html>