|
<!DOCTYPE html>
|
|
<html lang="zh-Hant">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>MES系統 - 掃描列印站</title>
|
|
<style>
|
|
:root{
|
|
--blue-900:#1e3a8a; --blue-800:#1e40af; --blue-700:#1d4ed8; --blue-600:#2563eb; --blue-500:#3b82f6;
|
|
--gray-50:#f8fafc; --gray-100:#f1f5f9; --gray-200:#e2e8f0; --gray-300:#cbd5e1; --gray-500:#64748b; --gray-600:#475569; --gray-800:#1e293b;
|
|
--green-600:#16a34a; --green-50:#f0fdf4;
|
|
--amber-600:#d97706; --amber-50:#fffbeb;
|
|
--purple-600:#7c3aed; --purple-50:#f5f3ff;
|
|
--red-600:#dc2626; --red-50:#fef2f2;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang TC","Microsoft JhengHei",sans-serif;background:var(--gray-100);color:var(--gray-800);display:flex;min-height:100vh;font-size:14px;}
|
|
|
|
.sidebar{width:220px;background:linear-gradient(180deg,var(--blue-900),var(--blue-800));color:#fff;flex-shrink:0;display:flex;flex-direction:column;}
|
|
.sidebar-header{padding:16px 18px;font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.12);}
|
|
.sidebar-header span.ver{font-size:11px;opacity:.6;font-weight:400;}
|
|
.nav-item{padding:12px 18px;font-size:13.5px;cursor:default;opacity:.85;border-left:3px solid transparent;}
|
|
.nav-item.active{background:rgba(255,255,255,.1);opacity:1;border-left-color:#fff;font-weight:600;}
|
|
.nav-sub{padding:10px 18px 10px 40px;font-size:13px;opacity:.85;}
|
|
.nav-sub.current{background:rgba(255,255,255,.15);opacity:1;font-weight:600;border-left:3px solid #fff;}
|
|
.sidebar-footer{margin-top:auto;padding:14px 18px;border-top:1px solid rgba(255,255,255,.12);display:flex;align-items:center;gap:10px;font-size:13px;}
|
|
.avatar{width:28px;height:28px;border-radius:50%;background:rgba(255,255,255,.2);display:flex;align-items:center;justify-content:center;font-size:12px;}
|
|
|
|
.main{flex:1;display:flex;flex-direction:column;min-width:0;}
|
|
.topbar{background:#fff;padding:16px 28px;font-size:19px;font-weight:700;border-bottom:1px solid var(--gray-200);display:flex;align-items:center;justify-content:space-between;}
|
|
.topbar .breadcrumb{font-size:12.5px;font-weight:400;color:var(--gray-500);margin-top:4px;}
|
|
.device-info{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:400;color:var(--gray-500);}
|
|
.device-info .dot{width:8px;height:8px;border-radius:50%;background:var(--green-600);}
|
|
.content{padding:20px 28px 40px;display:flex;flex-direction:column;gap:16px;}
|
|
|
|
.tab-bar{display:flex;gap:4px;background:#fff;border-radius:10px;border:1px solid var(--gray-200);padding:4px;width:fit-content;}
|
|
.tab-btn{padding:9px 18px;border-radius:8px;border:none;background:transparent;color:var(--gray-600);font-size:13.5px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:8px;white-space:nowrap;}
|
|
.tab-btn:hover{background:var(--gray-50);}
|
|
.tab-btn.active{background:var(--blue-600);color:#fff;}
|
|
.tab-btn.active:hover{background:var(--blue-600);}
|
|
.tab-btn .badge{background:var(--gray-200);color:var(--gray-600);border-radius:999px;padding:1px 8px;font-size:11.5px;font-weight:700;}
|
|
.tab-btn.active .badge{background:rgba(255,255,255,.25);color:#fff;}
|
|
.tab-panel{display:flex;flex-direction:column;gap:16px;}
|
|
|
|
.card{background:#fff;border-radius:10px;border:1px solid var(--gray-200);box-shadow:0 1px 2px rgba(0,0,0,.03);}
|
|
.card-head{padding:16px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--gray-200);gap:16px;flex-wrap:wrap;}
|
|
.card-head h2{margin:0;font-size:15.5px;}
|
|
.card-head p{margin:3px 0 0;font-size:12.5px;color:var(--gray-500);font-weight:400;}
|
|
.card-head .actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
|
|
|
|
.btn{border:none;border-radius:6px;padding:9px 16px;font-size:13.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:filter .15s;white-space:nowrap;}
|
|
.btn:hover{filter:brightness(0.93);}
|
|
.btn:active{filter:brightness(0.85);}
|
|
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none;}
|
|
.btn-green{background:var(--green-600);color:#fff;}
|
|
.btn-blue{background:var(--blue-600);color:#fff;}
|
|
.btn-gray{background:var(--gray-200);color:var(--gray-600);}
|
|
.btn-danger{background:var(--red-600);color:#fff;}
|
|
.btn-sm{padding:6px 12px;font-size:12.5px;}
|
|
|
|
/* Scan panel */
|
|
.scan-panel{padding:18px 24px;display:flex;flex-direction:column;gap:8px;}
|
|
.scan-bar{display:flex;align-items:center;gap:10px;width:100%;}
|
|
.scan-bar label{font-size:13.5px;color:var(--gray-600);font-weight:600;white-space:nowrap;}
|
|
.scan-input-field{
|
|
flex:1;padding:10px 14px;font-size:14px;font-family:Consolas,monospace;
|
|
border:1px solid var(--gray-300);border-radius:8px;
|
|
transition:border-color .15s,box-shadow .15s;
|
|
}
|
|
.scan-input-field:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
|
|
.scan-input-field.err{border-color:var(--red-600);box-shadow:0 0 0 3px rgba(220,38,38,.12);}
|
|
.scan-input-field.ok{border-color:var(--green-600);box-shadow:0 0 0 3px rgba(22,163,74,.12);}
|
|
|
|
.scan-status-line{font-size:12.5px;min-height:18px;display:flex;align-items:center;gap:6px;color:var(--gray-500);}
|
|
.scan-status-line.processing{color:var(--blue-700);}
|
|
.scan-status-line.success{color:var(--green-600);}
|
|
.scan-status-line.error{color:var(--red-600);}
|
|
|
|
/* Tables (shared) */
|
|
table{width:100%;border-collapse:collapse;}
|
|
thead th{text-align:left;font-size:12.5px;color:var(--gray-500);font-weight:600;padding:12px 16px;border-bottom:1px solid var(--gray-200);white-space:nowrap;background:#fff;}
|
|
tbody td{padding:14px 16px;border-bottom:1px solid var(--gray-100);font-size:13.5px;vertical-align:middle;}
|
|
tbody tr:hover{background:var(--gray-50);}
|
|
.mono{font-family:Consolas,monospace;color:var(--gray-800);}
|
|
.tag{display:inline-block;padding:3px 10px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.3px;white-space:nowrap;}
|
|
.tag-ok{background:var(--green-50);color:var(--green-600);border:1px solid #bbf7d0;}
|
|
.tag-myok{background:var(--amber-50);color:var(--amber-600);border:1px solid #fde68a;}
|
|
.tag-rgp{background:var(--purple-50);color:var(--purple-600);border:1px solid #ddd6fe;}
|
|
.result-pill{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;padding:4px 10px;border-radius:999px;white-space:nowrap;}
|
|
.result-pill.ok{background:var(--green-50);color:var(--green-600);}
|
|
.result-pill.warn{background:var(--amber-50);color:var(--amber-600);}
|
|
.result-pill.err{background:var(--red-50);color:var(--red-600);}
|
|
.empty-log{padding:40px 20px;text-align:center;color:var(--gray-500);font-size:13px;}
|
|
.row-btn{border:1px solid var(--gray-300);background:#fff;color:var(--red-600);border-radius:6px;padding:5px 10px;font-size:11.5px;cursor:pointer;}
|
|
.row-btn:hover{background:var(--red-50);border-color:#fecaca;}
|
|
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--gray-800);color:#fff;padding:12px 18px;border-radius:8px;font-size:13.5px;box-shadow:0 8px 20px rgba(0,0,0,.25);display:none;z-index:60;align-items:center;gap:8px;}
|
|
.toast.show{display:flex;}
|
|
|
|
.modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;z-index:50;}
|
|
.modal-overlay.show{display:flex;}
|
|
.modal{background:#fff;border-radius:12px;width:460px;max-width:92vw;padding:28px 26px;text-align:center;box-shadow:0 20px 50px rgba(0,0,0,.25);}
|
|
.modal h3{margin:0 0 6px;font-size:16px;}
|
|
.modal p{margin:0 0 4px;color:var(--gray-500);font-size:13px;}
|
|
.spinner{width:42px;height:42px;border:4px solid var(--gray-200);border-top-color:var(--purple-600);border-radius:50%;margin:18px auto 14px;animation:spin .8s linear infinite;}
|
|
@keyframes spin{to{transform:rotate(360deg);}}
|
|
.check-circle{width:52px;height:52px;border-radius:50%;background:var(--green-50);color:var(--green-600);display:flex;align-items:center;justify-content:center;margin:14px auto 16px;font-size:28px;}
|
|
.cross-circle{width:52px;height:52px;border-radius:50%;background:var(--red-50);color:var(--red-600);display:flex;align-items:center;justify-content:center;margin:14px auto 16px;font-size:28px;}
|
|
.modal-log{text-align:left;background:var(--gray-50);border-radius:8px;padding:12px 14px;margin:14px 0;font-size:12px;font-family:Consolas,monospace;color:var(--gray-600);max-height:220px;overflow-y:auto;}
|
|
.modal-log div{padding:2px 0;}
|
|
.modal-close{margin-top:10px;width:100%;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">MES系統 <span class="ver">v1.2.1</span></div>
|
|
<div class="nav-item">首頁儀表板</div>
|
|
<div class="nav-item">部門管理</div>
|
|
<div class="nav-item">人員/權限管理</div>
|
|
<div class="nav-item">物料管理</div>
|
|
<div class="nav-item">設備管理</div>
|
|
<div class="nav-item">設備維修管理</div>
|
|
<div class="nav-item">品質管理</div>
|
|
<div class="nav-item">異常管理</div>
|
|
<div class="nav-item">課程模組</div>
|
|
<div class="nav-item active">製造執行中心</div>
|
|
<div class="nav-sub">工單管理</div>
|
|
<div class="nav-sub">工單生產紀錄查詢</div>
|
|
<div class="nav-sub">生產即時監控看板</div>
|
|
<div class="nav-sub">工單列印設定</div>
|
|
<div class="nav-sub">標籤樣式設定</div>
|
|
<div class="nav-sub current">入庫標籤列印</div>
|
|
<div class="nav-item">刀具管理</div>
|
|
<div class="sidebar-footer">
|
|
<div class="avatar">👤</div>
|
|
<div>admin<br><span style="opacity:.6;font-size:11px;">admin</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<div class="topbar">
|
|
<div>
|
|
入庫標籤列印
|
|
<div class="breadcrumb">製造執行中心 / 入庫標籤列印</div>
|
|
</div>
|
|
<div class="device-info"><span class="dot"></span>掃描槍已連接(USB HID 鍵盤模式)</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
|
|
<div class="tab-bar">
|
|
<button class="tab-btn active" id="tabBtnScan" data-tab="scan">① 掃描列印標籤</button>
|
|
<button class="tab-btn" id="tabBtnReprint" data-tab="reprint">② 重新列印標籤</button>
|
|
<button class="tab-btn" id="tabBtnErp" data-tab="erp">③ 回送 ERP <span class="badge" id="erpBadge">0</span></button>
|
|
</div>
|
|
|
|
<!-- ============ Tab 1:掃描 → 送出工單 → 列印 ============ -->
|
|
<div class="tab-panel" id="tabScan">
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>條碼掃描 → 呼叫列印 API</h2>
|
|
<p>將游標保持在掃描欄位,以掃描槍刷過工單條碼即會自動送出(結尾送出 Enter),系統會查核工單並呼叫列印標籤 API</p>
|
|
</div>
|
|
</div>
|
|
<div class="scan-panel">
|
|
<div class="scan-bar">
|
|
<label for="scanInput">條碼刷入/輸入:</label>
|
|
<input type="text" id="scanInput" class="scan-input-field" placeholder="請掃描條碼或輸入工單編號後按 Enter(例如:A511-2606220001)" autocomplete="off">
|
|
<button class="btn btn-blue btn-sm" id="simulateScanBtn">模擬掃描</button>
|
|
</div>
|
|
<div class="scan-status-line" id="scanStatusLine"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>掃描/列印紀錄</h2>
|
|
<p>僅列出成功列印紀錄;若發生列印失敗會跳出提示 Dialog,請排除問題後重新掃描</p>
|
|
</div>
|
|
<span style="font-size:12px;color:var(--gray-500);">共 <span id="logCount">0</span> 筆紀錄</span>
|
|
</div>
|
|
<div style="overflow-x:auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>時間</th>
|
|
<th>條碼/工單編號</th>
|
|
<th>客戶</th>
|
|
<th>類別</th>
|
|
<th>結果</th>
|
|
<th>備註</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="empty-log" id="emptyLog">尚無掃描紀錄</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ============ Tab 2:重新列印標籤 ============ -->
|
|
<div class="tab-panel" id="tabReprint" style="display:none;">
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>重新列印標籤(專用)</h2>
|
|
<p>專門執行已列印過的工單重複補印,此頁籤重印的資料<b>不會</b>新增或傳送到「回送 ERP」清單中</p>
|
|
</div>
|
|
</div>
|
|
<div class="scan-panel">
|
|
<div class="scan-bar">
|
|
<label for="reprintInput">重印條碼刷入:</label>
|
|
<input type="text" id="reprintInput" class="scan-input-field" placeholder="請掃描已列印過之條碼或輸入工單編號後按 Enter" autocomplete="off">
|
|
<button class="btn btn-blue btn-sm" id="simulateReprintBtn">模擬重印掃描</button>
|
|
</div>
|
|
<div class="scan-status-line" id="reprintStatusLine"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>重新列印紀錄</h2>
|
|
<p>僅顯示在此頁籤執行的補印與重印歷史紀錄</p>
|
|
</div>
|
|
<span style="font-size:12px;color:var(--gray-500);">共 <span id="reprintLogCount">0</span> 筆紀錄</span>
|
|
</div>
|
|
<div style="overflow-x:auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>時間</th>
|
|
<th>條碼/工單編號</th>
|
|
<th>客戶</th>
|
|
<th>類別</th>
|
|
<th>結果</th>
|
|
<th>備註</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="reprintLogBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="empty-log" id="reprintEmptyLog">尚無重新列印紀錄</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ============ Tab 3:回送 ERP ============ -->
|
|
<div class="tab-panel" id="tabErp" style="display:none;">
|
|
|
|
<div class="card">
|
|
<div class="card-head">
|
|
<div>
|
|
<h2>回送 ERP</h2>
|
|
<p>僅列出「已完工」且「已成功刷入並正確列印標籤」的工單;可單選或多選後一次送出,送出結果(成功/失敗)會直接反映在「回送狀態」欄位,失敗的工單可重新勾選送出</p>
|
|
</div>
|
|
<div class="actions">
|
|
<span style="font-size:12px;color:var(--gray-500);">已勾選 <span id="erpSelectedCount">0</span> 筆</span>
|
|
<button class="btn btn-green btn-sm" id="submitErpBtn">📤 送出至 ERP</button>
|
|
</div>
|
|
</div>
|
|
<div style="overflow-x:auto;">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px;"><input type="checkbox" id="erpSelectAll"></th>
|
|
<th>工單編號</th>
|
|
<th>客戶名稱</th>
|
|
<th>料號</th>
|
|
<th>顏色</th>
|
|
<th>建立日期</th>
|
|
<th>完工日期</th>
|
|
<th>標籤列印時間</th>
|
|
<th>回送狀態</th>
|
|
<th style="width:70px;">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="erpQueueBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="empty-log" id="erpEmptyQueue" style="display:none;">目前沒有已完工且已正確列印標籤的工單</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 通用 Modal 彈窗 (含列印失敗/ERP處理結果) -->
|
|
<div class="modal-overlay" id="actionModal">
|
|
<div class="modal">
|
|
<div id="modalLoading">
|
|
<h3 id="loadingTitle">處理中</h3>
|
|
<p id="loadingDesc">請稍候…</p>
|
|
<div class="spinner"></div>
|
|
<div class="modal-log" id="loadingLog"></div>
|
|
</div>
|
|
<div id="modalResult" style="display:none;">
|
|
<div id="resultIcon"></div>
|
|
<h3 id="resultTitle"></h3>
|
|
<p id="resultDesc"></p>
|
|
<div class="modal-log" id="resultLog"></div>
|
|
<button class="btn btn-blue modal-close" id="modalCloseBtn">關閉</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
function pad2(n){ return n.toString().padStart(2,"0"); }
|
|
function fmtDateTime(d){
|
|
return `${d.getFullYear()}/${pad2(d.getMonth()+1)}/${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
}
|
|
function nowStr(){ return fmtDateTime(new Date()); }
|
|
|
|
// ---- 模擬工單資料 ----
|
|
const customers = ["大學台中西屯","大學新竹竹北","晴美-三峽","伯特利台南","卡爾蔡司(广州)","精華光學股份有限公司","晶碩光學","科妍生技"];
|
|
const categories = ["OK","MYOK","RGP"];
|
|
const prefixes = ["A511","A512","A513"];
|
|
const tagClass = {OK:"tag-ok",MYOK:"tag-myok",RGP:"tag-rgp"};
|
|
const materialCodes = ["FKMK5340041","FKMK5440041","FKMK5440201","FRHC0306101","FKRP3300012","FKOK4400033"];
|
|
const colors = [
|
|
{name:"紫色",en:"Violet"}, {name:"綠色",en:"Green"}, {name:"藍色",en:"Blue"},
|
|
{name:"透明",en:"Clear"}, {name:"琥珀",en:"Amber"}
|
|
];
|
|
|
|
function pad(n){return n.toString().padStart(4,"0");}
|
|
function genData(n){
|
|
const rows=[];
|
|
const base = new Date(2026,5,22,14,39,52);
|
|
for(let i=1;i<=n;i++){
|
|
const prefix = prefixes[i%prefixes.length];
|
|
const cat = categories[i%categories.length];
|
|
const color = colors[i%colors.length];
|
|
const createdAt = new Date(base.getTime() + i*61*1000);
|
|
const completedAt = new Date(createdAt.getTime() + (2*3600 + 3*60)*1000);
|
|
rows.push({
|
|
id: `${prefix}-2606${pad(220000+i).slice(2)}`,
|
|
customer: customers[i%customers.length],
|
|
category: cat,
|
|
materialCode: materialCodes[i%materialCodes.length],
|
|
color: `${color.name}(${color.en})`,
|
|
createdAt: fmtDateTime(createdAt),
|
|
completedAt: fmtDateTime(completedAt),
|
|
printed: false,
|
|
printedAt: null,
|
|
erpSent: false,
|
|
erpRemoved: false,
|
|
erpLastResult: null,
|
|
erpLastTime: null,
|
|
erpLastNote: ""
|
|
});
|
|
}
|
|
return rows;
|
|
}
|
|
const allData = genData(30);
|
|
function findOrder(code){
|
|
const c = code.trim().toUpperCase();
|
|
return allData.find(r=>r.id.toUpperCase()===c);
|
|
}
|
|
|
|
// ============================================================
|
|
// ---- Dialog 彈窗控制 ----
|
|
// ============================================================
|
|
const actionModal = document.getElementById("actionModal");
|
|
const modalLoading = document.getElementById("modalLoading");
|
|
const modalResult = document.getElementById("modalResult");
|
|
|
|
function showErrorModal({title, desc, log}){
|
|
actionModal.classList.add("show");
|
|
modalLoading.style.display = "none";
|
|
modalResult.style.display = "block";
|
|
document.getElementById("resultIcon").innerHTML = `<div class="cross-circle">✕</div>`;
|
|
document.getElementById("resultTitle").textContent = title;
|
|
document.getElementById("resultDesc").textContent = desc;
|
|
document.getElementById("resultLog").innerHTML = (log||[]).map(l=>`<div>${l}</div>`).join("");
|
|
}
|
|
|
|
function runModalSequence({title, desc, steps, onDone}){
|
|
actionModal.classList.add("show");
|
|
modalLoading.style.display="block";
|
|
modalResult.style.display="none";
|
|
document.getElementById("loadingTitle").textContent = title;
|
|
document.getElementById("loadingDesc").textContent = desc;
|
|
const log = document.getElementById("loadingLog");
|
|
log.innerHTML = "";
|
|
let i=0;
|
|
const interval = setInterval(()=>{
|
|
if(i<steps.length){
|
|
const line = document.createElement("div");
|
|
line.textContent = "› " + steps[i];
|
|
log.appendChild(line);
|
|
log.scrollTop = log.scrollHeight;
|
|
i++;
|
|
} else {
|
|
clearInterval(interval);
|
|
setTimeout(onDone, 400);
|
|
}
|
|
}, 260);
|
|
}
|
|
|
|
function showModalResult({ok, title, desc, log}){
|
|
modalLoading.style.display="none";
|
|
modalResult.style.display="block";
|
|
document.getElementById("resultIcon").innerHTML = ok
|
|
? `<div class="check-circle">✓</div>` : `<div class="cross-circle">✕</div>`;
|
|
document.getElementById("resultTitle").textContent = title;
|
|
document.getElementById("resultDesc").textContent = desc;
|
|
document.getElementById("resultLog").innerHTML = (log||[]).map(l=>`<div>${l}</div>`).join("");
|
|
}
|
|
|
|
document.getElementById("modalCloseBtn").addEventListener("click", ()=>{
|
|
actionModal.classList.remove("show");
|
|
if(activeTab === "scan") scanInput.focus();
|
|
if(activeTab === "reprint") reprintInput.focus();
|
|
});
|
|
|
|
// ============================================================
|
|
// ---- Tab 切換 ----
|
|
// ============================================================
|
|
let activeTab = "scan";
|
|
function switchTab(tab){
|
|
activeTab = tab;
|
|
document.getElementById("tabScan").style.display = tab==="scan" ? "flex" : "none";
|
|
document.getElementById("tabReprint").style.display = tab==="reprint" ? "flex" : "none";
|
|
document.getElementById("tabErp").style.display = tab==="erp" ? "flex" : "none";
|
|
|
|
document.getElementById("tabBtnScan").classList.toggle("active", tab==="scan");
|
|
document.getElementById("tabBtnReprint").classList.toggle("active", tab==="reprint");
|
|
document.getElementById("tabBtnErp").classList.toggle("active", tab==="erp");
|
|
|
|
if(tab==="scan"){
|
|
setTimeout(()=> scanInput.focus(), 0);
|
|
} else if(tab==="reprint"){
|
|
setTimeout(()=> reprintInput.focus(), 0);
|
|
} else {
|
|
renderErpQueue();
|
|
}
|
|
}
|
|
document.getElementById("tabBtnScan").addEventListener("click", ()=> switchTab("scan"));
|
|
document.getElementById("tabBtnReprint").addEventListener("click", ()=> switchTab("reprint"));
|
|
document.getElementById("tabBtnErp").addEventListener("click", ()=> switchTab("erp"));
|
|
|
|
function updateErpBadge(){
|
|
const count = allData.filter(o=>o.printed && !o.erpSent && !o.erpRemoved).length;
|
|
document.getElementById("erpBadge").textContent = count;
|
|
}
|
|
|
|
// ============================================================
|
|
// ---- Tab 1:掃描列印標籤 ----
|
|
// ============================================================
|
|
const scanInput = document.getElementById("scanInput");
|
|
const scanStatusLine = document.getElementById("scanStatusLine");
|
|
const logBody = document.getElementById("logBody");
|
|
const emptyLog = document.getElementById("emptyLog");
|
|
let logEntries = [];
|
|
|
|
function setStatus(mode, main){
|
|
const icons = { idle:"", processing:"⏳", success:"✓", error:"✕" };
|
|
scanStatusLine.className = "scan-status-line " + mode;
|
|
scanStatusLine.textContent = main ? `${icons[mode]||""} ${main}`.trim() : "";
|
|
scanInput.classList.remove("ok","err");
|
|
if(mode==="success") scanInput.classList.add("ok");
|
|
if(mode==="error") scanInput.classList.add("err");
|
|
}
|
|
|
|
function addLogEntry(entry){
|
|
logEntries.unshift(entry);
|
|
document.getElementById("logCount").textContent = logEntries.length;
|
|
emptyLog.style.display = logEntries.length ? "none" : "block";
|
|
|
|
const tr = document.createElement("tr");
|
|
const catTag = entry.category ? `<span class="tag ${tagClass[entry.category]}">${entry.category}</span>` : "—";
|
|
let pillHtml = `<span class="result-pill ok">✓ 已完成</span>`;
|
|
|
|
tr.innerHTML = `
|
|
<td class="mono">${entry.time}</td>
|
|
<td class="mono">${entry.code}</td>
|
|
<td>${entry.customer || "—"}</td>
|
|
<td>${catTag}</td>
|
|
<td>${pillHtml}</td>
|
|
<td style="color:var(--gray-500);">${entry.note || ""}</td>
|
|
`;
|
|
logBody.insertBefore(tr, logBody.firstChild);
|
|
}
|
|
|
|
let processing = false;
|
|
function processScan(rawCode){
|
|
if(processing) return;
|
|
const code = rawCode.trim();
|
|
if(!code) return;
|
|
|
|
const order = findOrder(code);
|
|
if(!order){
|
|
showErrorModal({
|
|
title: "無效的工單條碼",
|
|
desc: "請檢查條碼是否正確,排除問題後再重新掃描",
|
|
log: [`條碼:${code}`, "原因:系統中查無此工單編號"]
|
|
});
|
|
resetInputSoon(scanInput, "scan");
|
|
return;
|
|
}
|
|
|
|
if(order.printed){
|
|
showErrorModal({
|
|
title: "重複列印阻擋",
|
|
desc: "該工單先前已完成列印,請至「重新列印標籤」頁籤執行補印",
|
|
log: [`工單編號:${order.id}`, `上次列印時間:${order.printedAt || "已列印"}`]
|
|
});
|
|
resetInputSoon(scanInput, "scan");
|
|
return;
|
|
}
|
|
|
|
processing = true;
|
|
setStatus("processing", `掃描到工單 ${order.id},正在呼叫列印 API...`);
|
|
|
|
setTimeout(()=>{
|
|
const apiFail = Math.random() < 0.15;
|
|
if(apiFail){
|
|
const reasons = ["BarTender API 逾時,請重新掃描", "印表機離線或無回應,請確認印表機狀態", "紙張/碳帶不足,請補充耗材後重新掃描"];
|
|
const reason = reasons[Math.floor(Math.random()*reasons.length)];
|
|
setStatus("error", `工單 ${order.id} 列印失敗`);
|
|
|
|
showErrorModal({
|
|
title: "標籤列印失敗",
|
|
desc: "請檢查硬體或設備問題原因,檢查完畢後請再次重新掃描",
|
|
log: [`工單編號:${order.id}`, `失敗原因:${reason}`]
|
|
});
|
|
} else {
|
|
order.printed = true;
|
|
order.printedAt = nowStr();
|
|
updateErpBadge();
|
|
setStatus("success", `工單 ${order.id} 列印完成,已加入待回送 ERP 清單`);
|
|
addLogEntry({ time: nowStr(), code: order.id, customer: order.customer, category: order.category, result:"ok", note:"" });
|
|
}
|
|
processing = false;
|
|
resetInputSoon(scanInput, "scan");
|
|
}, 700);
|
|
}
|
|
|
|
scanInput.addEventListener("keydown", e=>{
|
|
if(e.key==="Enter"){
|
|
e.preventDefault();
|
|
processScan(scanInput.value);
|
|
}
|
|
});
|
|
|
|
document.getElementById("simulateScanBtn").addEventListener("click", ()=>{
|
|
const unprintedList = allData.filter(o=>!o.printed);
|
|
const target = unprintedList.length > 0 ? unprintedList[Math.floor(Math.random()*unprintedList.length)] : allData[0];
|
|
scanInput.value = target.id;
|
|
processScan(target.id);
|
|
});
|
|
|
|
// ============================================================
|
|
// ---- Tab 2:重新列印標籤 ----
|
|
// ============================================================
|
|
const reprintInput = document.getElementById("reprintInput");
|
|
const reprintStatusLine = document.getElementById("reprintStatusLine");
|
|
const reprintLogBody = document.getElementById("reprintLogBody");
|
|
const reprintEmptyLog = document.getElementById("reprintEmptyLog");
|
|
let reprintLogEntries = [];
|
|
|
|
function setReprintStatus(mode, main){
|
|
const icons = { idle:"", processing:"⏳", success:"✓", error:"✕" };
|
|
reprintStatusLine.className = "scan-status-line " + mode;
|
|
reprintStatusLine.textContent = main ? `${icons[mode]||""} ${main}`.trim() : "";
|
|
reprintInput.classList.remove("ok","err");
|
|
if(mode==="success") reprintInput.classList.add("ok");
|
|
if(mode==="error") reprintInput.classList.add("err");
|
|
}
|
|
|
|
function addReprintLogEntry(entry){
|
|
reprintLogEntries.unshift(entry);
|
|
document.getElementById("reprintLogCount").textContent = reprintLogEntries.length;
|
|
reprintEmptyLog.style.display = reprintLogEntries.length ? "none" : "block";
|
|
|
|
const tr = document.createElement("tr");
|
|
const catTag = entry.category ? `<span class="tag ${tagClass[entry.category]}">${entry.category}</span>` : "—";
|
|
let pillHtml = `<span class="result-pill warn">⚠ 已重新列印</span>`;
|
|
|
|
tr.innerHTML = `
|
|
<td class="mono">${entry.time}</td>
|
|
<td class="mono">${entry.code}</td>
|
|
<td>${entry.customer || "—"}</td>
|
|
<td>${catTag}</td>
|
|
<td>${pillHtml}</td>
|
|
<td style="color:var(--amber-600);">${entry.note || ""}</td>
|
|
`;
|
|
reprintLogBody.insertBefore(tr, reprintLogBody.firstChild);
|
|
}
|
|
|
|
let reprintProcessing = false;
|
|
function processReprint(rawCode){
|
|
if(reprintProcessing) return;
|
|
const code = rawCode.trim();
|
|
if(!code) return;
|
|
|
|
const order = findOrder(code);
|
|
if(!order){
|
|
showErrorModal({
|
|
title: "無效的工單條碼",
|
|
desc: "請檢查條碼是否正確,排除問題後再重新掃描",
|
|
log: [`條碼:${code}`, "原因:系統中查無此工單編號"]
|
|
});
|
|
resetInputSoon(reprintInput, "reprint");
|
|
return;
|
|
}
|
|
|
|
reprintProcessing = true;
|
|
setReprintStatus("processing", `正在為工單 ${order.id} 執行重新列印 API...`);
|
|
|
|
setTimeout(()=>{
|
|
const apiFail = Math.random() < 0.15;
|
|
if(apiFail){
|
|
const reasons = ["BarTender API 逾時,請重新掃描", "印表機離線或無回應,請確認印表機狀態"];
|
|
const reason = reasons[Math.floor(Math.random()*reasons.length)];
|
|
setReprintStatus("error", `工單 ${order.id} 重新列印失敗`);
|
|
|
|
showErrorModal({
|
|
title: "重新列印失敗",
|
|
desc: "請檢查硬體或設備問題原因,檢查完畢後請再次重新掃描",
|
|
log: [`工單編號:${order.id}`, `失敗原因:${reason}`]
|
|
});
|
|
} else {
|
|
setReprintStatus("success", `工單 ${order.id} 重新列印完成(不會影響 ERP 回送狀態)`);
|
|
addReprintLogEntry({ time: nowStr(), code: order.id, customer: order.customer, category: order.category, result:"ok", note:"重複列印(不入 ERP)" });
|
|
}
|
|
reprintProcessing = false;
|
|
resetInputSoon(reprintInput, "reprint");
|
|
}, 700);
|
|
}
|
|
|
|
reprintInput.addEventListener("keydown", e=>{
|
|
if(e.key==="Enter"){
|
|
e.preventDefault();
|
|
processReprint(reprintInput.value);
|
|
}
|
|
});
|
|
|
|
document.getElementById("simulateReprintBtn").addEventListener("click", ()=>{
|
|
const printedList = allData.filter(o=>o.printed);
|
|
const target = printedList.length > 0 ? printedList[Math.floor(Math.random()*printedList.length)] : allData[0];
|
|
reprintInput.value = target.id;
|
|
processReprint(target.id);
|
|
});
|
|
|
|
// 通用輸入框重置與聚焦處理
|
|
function resetInputSoon(inputElem, tabName){
|
|
inputElem.value = "";
|
|
setTimeout(()=>{
|
|
if(activeTab === tabName) inputElem.focus();
|
|
}, 60);
|
|
clearTimeout(window.__statusResetTimer);
|
|
window.__statusResetTimer = setTimeout(()=>{
|
|
if(!processing && !reprintProcessing){
|
|
setStatus("idle", "");
|
|
setReprintStatus("idle", "");
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
// 保持焦點於當前頁籤之輸入框
|
|
document.addEventListener("click", e=>{
|
|
if(actionModal.classList.contains("show")) return;
|
|
if(activeTab==="scan"){
|
|
setTimeout(()=> scanInput.focus(), 0);
|
|
} else if(activeTab==="reprint"){
|
|
setTimeout(()=> reprintInput.focus(), 0);
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// ---- Tab 3:回送 ERP ----
|
|
// ============================================================
|
|
let erpSelected = new Set();
|
|
|
|
function erpQueueData(){
|
|
return allData.filter(o=>o.printed && !o.erpRemoved);
|
|
}
|
|
|
|
function renderErpQueue(){
|
|
const queue = erpQueueData();
|
|
const body = document.getElementById("erpQueueBody");
|
|
body.innerHTML = "";
|
|
|
|
const selectableIds = new Set(queue.filter(o=>!o.erpSent).map(o=>o.id));
|
|
erpSelected.forEach(id=>{ if(!selectableIds.has(id)) erpSelected.delete(id); });
|
|
|
|
queue.forEach(o=>{
|
|
const tr = document.createElement("tr");
|
|
let statusHtml, actionHtml;
|
|
if(o.erpSent){
|
|
statusHtml = `<span class="result-pill ok">✓ 回送成功</span><div style="color:var(--gray-500);font-size:11.5px;margin-top:3px;">${o.erpLastTime}</div>`;
|
|
actionHtml = `<span style="color:var(--gray-300);">—</span>`;
|
|
} else if(o.erpLastResult==="err"){
|
|
statusHtml = `<span class="result-pill err">✕ 回送失敗</span><div style="color:var(--red-600);font-size:11.5px;margin-top:3px;">${o.erpLastNote}</div>`;
|
|
actionHtml = `<button class="row-btn" data-id="${o.id}">移除</button>`;
|
|
} else {
|
|
statusHtml = `<span class="result-pill warn" style="background:var(--gray-100);color:var(--gray-600);">待回送</span>`;
|
|
actionHtml = `<button class="row-btn" data-id="${o.id}">移除</button>`;
|
|
}
|
|
tr.innerHTML = `
|
|
<td>${o.erpSent ? "" : `<input type="checkbox" class="erp-row-check" data-id="${o.id}" ${erpSelected.has(o.id)?"checked":""}>`}</td>
|
|
<td class="mono">${o.id}</td>
|
|
<td>${o.customer}</td>
|
|
<td class="mono">${o.materialCode}</td>
|
|
<td>${o.color}</td>
|
|
<td class="mono">${o.createdAt}</td>
|
|
<td class="mono">${o.completedAt}</td>
|
|
<td class="mono">${o.printedAt || "—"}</td>
|
|
<td>${statusHtml}</td>
|
|
<td>${actionHtml}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
|
|
document.getElementById("erpEmptyQueue").style.display = queue.length ? "none" : "block";
|
|
document.getElementById("erpSelectAll").checked = selectableIds.size>0 && erpSelected.size===selectableIds.size;
|
|
document.getElementById("erpSelectAll").indeterminate = erpSelected.size>0 && erpSelected.size<selectableIds.size;
|
|
document.getElementById("erpSelectedCount").textContent = erpSelected.size;
|
|
updateErpBadge();
|
|
}
|
|
|
|
document.getElementById("erpQueueBody").addEventListener("change", e=>{
|
|
if(!e.target.classList.contains("erp-row-check")) return;
|
|
const id = e.target.dataset.id;
|
|
if(e.target.checked) erpSelected.add(id); else erpSelected.delete(id);
|
|
document.getElementById("erpSelectedCount").textContent = erpSelected.size;
|
|
const selectableIds = new Set(erpQueueData().filter(o=>!o.erpSent).map(o=>o.id));
|
|
document.getElementById("erpSelectAll").checked = selectableIds.size>0 && erpSelected.size===selectableIds.size;
|
|
document.getElementById("erpSelectAll").indeterminate = erpSelected.size>0 && erpSelected.size<selectableIds.size;
|
|
});
|
|
|
|
document.getElementById("erpQueueBody").addEventListener("click", e=>{
|
|
const btn = e.target.closest("button[data-id]");
|
|
if(!btn) return;
|
|
const id = btn.dataset.id;
|
|
const order = allData.find(o=>o.id===id);
|
|
if(order) order.erpRemoved = true;
|
|
erpSelected.delete(id);
|
|
renderErpQueue();
|
|
showToast(`已將工單 ${id} 從清單移除`);
|
|
});
|
|
|
|
document.getElementById("erpSelectAll").addEventListener("change", e=>{
|
|
const selectable = erpQueueData().filter(o=>!o.erpSent);
|
|
if(e.target.checked){
|
|
selectable.forEach(o=> erpSelected.add(o.id));
|
|
} else {
|
|
erpSelected.clear();
|
|
}
|
|
renderErpQueue();
|
|
});
|
|
|
|
document.getElementById("submitErpBtn").addEventListener("click", ()=>{
|
|
if(erpSelected.size===0){
|
|
showToast("請至少勾選一筆工單再送出");
|
|
return;
|
|
}
|
|
const ids = Array.from(erpSelected);
|
|
runModalSequence({
|
|
title:"回送 ERP",
|
|
desc:`正在將 ${ids.length} 筆工單資料回送至 ERP 系統`,
|
|
steps:[
|
|
"驗證工單列印狀態...",
|
|
`呼叫 ERP 入庫回送 API(共 ${ids.length} 筆)...`,
|
|
"寫入 ERP 入庫紀錄..."
|
|
],
|
|
onDone: ()=>{
|
|
let okCount = 0, failCount = 0;
|
|
const resultLines = [];
|
|
ids.forEach(id=>{
|
|
const order = allData.find(o=>o.id===id);
|
|
if(!order) return;
|
|
const fail = Math.random() < 0.1;
|
|
if(fail){
|
|
failCount++;
|
|
order.erpLastResult = "err";
|
|
order.erpLastTime = nowStr();
|
|
order.erpLastNote = "ERP 系統回應逾時,請重新勾選送出";
|
|
resultLines.push(`✕ ${order.id} 回送失敗`);
|
|
} else {
|
|
okCount++;
|
|
order.erpSent = true;
|
|
order.erpLastResult = "ok";
|
|
order.erpLastTime = nowStr();
|
|
order.erpLastNote = "";
|
|
erpSelected.delete(id);
|
|
resultLines.push(`✓ ${order.id} 回送成功`);
|
|
}
|
|
});
|
|
showModalResult({
|
|
ok: failCount===0,
|
|
title: failCount===0 ? "回送 ERP 完成" : "回送 ERP 部分完成",
|
|
desc: `成功 ${okCount} 筆/失敗 ${failCount} 筆${failCount>0 ? ",失敗工單仍保留在待回送清單中,可重新勾選送出" : ""}`,
|
|
log: resultLines
|
|
});
|
|
renderErpQueue();
|
|
}
|
|
});
|
|
});
|
|
|
|
function showToast(msg){
|
|
const t = document.getElementById("toast");
|
|
t.textContent = "✓ " + msg;
|
|
t.classList.add("show");
|
|
setTimeout(()=>t.classList.remove("show"), 3000);
|
|
}
|
|
|
|
updateErpBadge();
|
|
scanInput.focus();
|
|
</script>
|
|
</body>
|
|
</html>
|