Project

General

Profile

Feature #224 » sv.mes.工單列印設定.v2.html

鎮源 游, 09/30/2026 07:15 AM

 
<!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);}
.topbar .breadcrumb{font-size:12.5px;font-weight:400;color:var(--gray-500);margin-top:4px;}
.content{padding:20px 28px 40px;display:flex;flex-direction:column;gap:20px;}

.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;}
.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;}

.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-outline{background:#fff;color:var(--blue-600);border:1px solid var(--blue-500);}
.btn-sm{padding:6px 12px;font-size:12.5px;}

.status-pill{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;padding:5px 12px;border-radius:999px;}
.status-ok{background:var(--green-50);color:var(--green-600);}
.status-fail{background:var(--red-50);color:var(--red-600);}
.status-dot{width:7px;height:7px;border-radius:50%;background:currentColor;}

.field{display:flex;flex-direction:column;gap:5px;}
.field label{font-size:12.5px;color:var(--gray-600);font-weight:600;}
.field input[type=text],.field select{padding:9px 12px;border:1px solid var(--gray-300);border-radius:6px;font-size:13.5px;background:#fff;width:100%;}
.field input:focus,.field select:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}

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);}

.tag{display:inline-block;padding:4px 12px;border-radius:999px;font-size:12px;font-weight:700;letter-spacing:.3px;}
.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;}
.tag-custom{background:var(--gray-100);color:var(--gray-600);border:1px solid var(--gray-300);}

select.api-select{padding:8px 10px;border:1px solid var(--gray-300);border-radius:6px;font-size:13px;min-width:220px;background:#fff;}
.endpoint-text{font-family:Consolas,monospace;font-size:12px;color:var(--gray-500);word-break:break-all;}

.tray-btn{
display:inline-flex;flex-direction:column;align-items:flex-start;gap:3px;
background:#fff;border:1px solid var(--gray-300);border-radius:8px;padding:8px 12px;cursor:pointer;font-size:12px;min-width:180px;
}
.tray-btn:hover{border-color:var(--blue-500);background:#f8fbff;}
.tray-btn .tray-title{font-weight:700;color:var(--gray-600);display:flex;align-items:center;gap:6px;}
.tray-btn .tray-text{font-family:Consolas,monospace;font-size:11px;color:var(--blue-600);}
.tray-btn .tray-text.muted{color:var(--gray-500);font-family:inherit;}

.radio-option{display:flex;align-items:flex-start;gap:8px;font-weight:400;font-size:13.5px;cursor:pointer;line-height:1.5;}
.radio-option input{margin-top:3px;}

.toggle{position:relative;display:inline-block;width:38px;height:22px;}
.toggle input{opacity:0;width:0;height:0;}
.toggle .slider{position:absolute;cursor:pointer;inset:0;background:var(--gray-300);border-radius:999px;transition:.2s;}
.toggle .slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s;}
.toggle input:checked + .slider{background:var(--green-600);}
.toggle input:checked + .slider:before{transform:translateX(16px);}

.row-actions{display:flex;gap:6px;}
.icon-btn{border:1px solid var(--gray-300);background:#fff;color:var(--gray-600);border-radius:6px;padding:6px 10px;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:4px;}
.icon-btn:hover{background:var(--gray-50);}
.icon-btn.test{color:var(--purple-600);border-color:#ddd6fe;}
.icon-btn.test:hover{background:var(--purple-50);}
.icon-btn.del{color:var(--red-600);border-color:#fecaca;}
.icon-btn.del:hover{background:var(--red-50);}

.note-box{display:flex;gap:10px;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:12px 14px;font-size:12.5px;color:#1e40af;line-height:1.6;margin:16px 20px 0;}
.note-box .ico{font-size:16px;}

.card-foot{display:flex;justify-content:space-between;align-items:center;padding:14px 20px;border-top:1px solid var(--gray-200);}
.card-foot .last-saved{font-size:12px;color:var(--gray-500);}

.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:420px;max-width:90vw;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:150px;overflow-y:auto;}
.modal-log div{padding:2px 0;}
.modal-close{margin-top:10px;width:100%;}

.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;}

.add-row-form{display:none;background:var(--gray-50);border:1px dashed var(--gray-300);border-radius:8px;padding:16px;margin:16px 20px 0;gap:12px;flex-wrap:wrap;align-items:flex-end;}
.add-row-form.show{display:flex;}
</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 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 class="breadcrumb">製造執行中心 / 工單列印設定</div>
</div>

<div class="content">

<div class="card">
<div class="card-head">
<div>
<h2>工單類別 ↔ 列印 API 綁定</h2>
<p>每個工單類別已各自建好對應的 BarTender API(.btw 已於後台建置完成)。送印時依此對應表決定呼叫哪一支 API;紙匣預設沿用 .btw 範本設定,也可在「紙匣設定」自訂印表機/紙匣於送印時覆寫。</p>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-outline btn-sm" id="addBindingBtn">+ 新增類別綁定</button>
<button class="btn btn-green btn-sm" id="saveAllBtn">儲存全部設定</button>
</div>
</div>

<div class="add-row-form" id="addRowForm">
<div class="field" style="min-width:160px;">
<label>工單類別代碼</label>
<input type="text" id="newCatCode" placeholder="例如 VIP">
</div>
<div class="field" style="min-width:260px;">
<label>對應 API</label>
<select class="api-select" id="newCatApi"></select>
</div>
<button class="btn btn-blue btn-sm" id="confirmAddBtn">加入</button>
<button class="btn btn-gray btn-sm" id="cancelAddBtn">取消</button>
</div>

<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>工單類別</th>
<th>對應 API</th>
<th>API Endpoint</th>
<th>紙匣設定</th>
<th>狀態</th>
<th>上次測試結果</th>
<th>操作</th>
</tr>
</thead>
<tbody id="bindingBody"></tbody>
</table>
</div>

<div class="card-foot">
<span class="last-saved" id="lastSaved">上次儲存:2026/08/09 上午 09:12</span>
<span class="last-saved">共 <span id="bindingCount">0</span> 筆類別綁定</span>
</div>
</div>

</div>
</div>

<!-- Tray / printer override modal -->
<div class="modal-overlay" id="trayModal">
<div class="modal wide">
<h3 id="trayModalTitle">紙匣 / 印表機設定</h3>
<p id="trayModalSub"></p>
<div style="text-align:left;margin-top:16px;display:flex;flex-direction:column;gap:14px;">
<div class="field">
<label>紙匣來源</label>
<div style="display:flex;flex-direction:column;gap:10px;margin-top:6px;">
<label class="radio-option">
<input type="radio" name="traySource" id="traySourceDefault" value="default">
<span>沿用 .btw 範本設定(預設,不覆寫印表機/紙匣,紙匣已在後台 .btw 建置時設定好)</span>
</label>
<label class="radio-option">
<input type="radio" name="traySource" id="traySourceCustom" value="custom">
<span>由此處指定印表機與紙匣(送印時覆寫 .btw 內的設定)</span>
</label>
</div>
</div>
<div id="trayCustomFields" style="display:none;flex-direction:column;gap:14px;">
<div class="field">
<label>印表機</label>
<input type="text" id="trayPrinter" placeholder="例如 Zebra ZT411 - 出貨包裝線">
</div>
<div class="field">
<label>紙匣 / 卡夾</label>
<select id="trayTraySelect">
<option value="">請選擇</option>
<option value="Tray 1">Tray 1</option>
<option value="Tray 2">Tray 2</option>
<option value="Tray 3">Tray 3</option>
<option value="自動選擇">自動選擇(印表機自動判斷)</option>
<option value="手動送紙">手動送紙(Manual Feed)</option>
<option value="__custom__">自訂...</option>
</select>
<input type="text" id="trayTrayCustom" placeholder="輸入自訂紙匣名稱" style="display:none;margin-top:8px;">
</div>
<div class="note-box" style="margin:0;">
<span class="ico">ℹ️</span>
<div>紙匣名稱由印表機驅動程式決定,不同印表機型號命名可能不同(如 Tray 1、Cassette 2),請與印表機管理者確認實際名稱後再填寫。</div>
</div>
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-gray" style="flex:1;" id="cancelTrayBtn">取消</button>
<button class="btn btn-green" style="flex:1;" id="saveTrayBtn">儲存</button>
</div>
</div>
</div>

<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>
// 已在 BarTender 後台建立好的 API 清單(每支 API 已對應各自的 .btw 範本 / 印表機)
const availableApis = [
{ name:"OK_Label_API", endpoint:"http://192.168.10.55:6608/api/print/OK_Label" },
{ name:"MYOK_Label_API", endpoint:"http://192.168.10.55:6608/api/print/MYOK_Label" },
{ name:"RGP_Label_API", endpoint:"http://192.168.10.55:6608/api/print/RGP_Label" },
{ name:"Backup_Label_API", endpoint:"http://192.168.10.55:6608/api/print/Backup_Label" }
];

function apiByName(name){ return availableApis.find(a=>a.name===name); }

const tagClass = {OK:"tag-ok",MYOK:"tag-myok",RGP:"tag-rgp"};
function classFor(cat){ return tagClass[cat] || "tag-custom"; }

let bindings = [
{ cat:"OK", api:"OK_Label_API", enabled:true, lastTest:{ok:true, time:"2026/08/08 17:20"}, traySource:"default", printer:"", tray:"" },
{ cat:"MYOK", api:"MYOK_Label_API", enabled:true, lastTest:{ok:true, time:"2026/08/08 17:21"}, traySource:"custom", printer:"Zebra ZT411 - 出貨包裝線", tray:"Tray 2" },
{ cat:"RGP", api:"RGP_Label_API", enabled:false, lastTest:{ok:false, time:"2026/08/07 11:03"}, traySource:"default", printer:"", tray:"" },
];

function populateApiSelect(select, selected){
select.innerHTML = availableApis.map(a=>`<option value="${a.name}" ${a.name===selected?"selected":""}>${a.name}</option>`).join("");
}

function renderBindings(){
const body = document.getElementById("bindingBody");
body.innerHTML = "";
bindings.forEach((b,idx)=>{
const tr = document.createElement("tr");
const ep = apiByName(b.api) ? apiByName(b.api).endpoint : "";
tr.innerHTML = `
<td><span class="tag ${classFor(b.cat)}">${b.cat}</span></td>
<td><select class="api-select" data-idx="${idx}" data-role="api"></select></td>
<td><span class="endpoint-text" data-idx="${idx}" data-role="endpoint">${ep}</span></td>
<td>
<button class="tray-btn" data-idx="${idx}" data-action="edit-tray">
<span class="tray-title">🖨️ 紙匣設定</span>
<span class="tray-text ${b.traySource==='custom' ? '' : 'muted'}">${b.traySource==='custom' ? `${b.tray || '未設定紙匣'} · ${b.printer || '未設定印表機'}` : '沿用 .btw 範本'}</span>
</button>
</td>
<td>
<label class="toggle">
<input type="checkbox" data-idx="${idx}" data-role="enabled" ${b.enabled?"checked":""}>
<span class="slider"></span>
</label>
</td>
<td>
${b.lastTest
? `<span class="status-pill ${b.lastTest.ok?"status-ok":"status-fail"}"><span class="status-dot"></span>${b.lastTest.ok?"成功":"失敗"} · ${b.lastTest.time}</span>`
: `<span style="color:var(--gray-500);font-size:12.5px;">尚未測試</span>`}
</td>
<td>
<div class="row-actions">
<button class="icon-btn test" data-idx="${idx}" data-action="test">🖨️ 測試列印</button>
<button class="icon-btn del" data-idx="${idx}" data-action="delete">🗑 移除</button>
</div>
</td>
`;
body.appendChild(tr);
const sel = tr.querySelector('select[data-role="api"]');
populateApiSelect(sel, b.api);
});
document.getElementById("bindingCount").textContent = bindings.length;
}

document.getElementById("bindingBody").addEventListener("change", e=>{
const idx = e.target.dataset.idx;
const role = e.target.dataset.role;
if(idx===undefined) return;
if(role==="api"){
bindings[idx].api = e.target.value;
const row = e.target.closest("tr");
row.querySelector('[data-role="endpoint"]').textContent = apiByName(e.target.value).endpoint;
}
if(role==="enabled") bindings[idx].enabled = e.target.checked;
});

document.getElementById("bindingBody").addEventListener("click", e=>{
const btn = e.target.closest("button[data-action]");
if(!btn) return;
const idx = btn.dataset.idx;
const action = btn.dataset.action;
if(action==="delete"){
const cat = bindings[idx].cat;
bindings.splice(idx,1);
renderBindings();
showToast(`已移除「${cat}」類別綁定`);
}
if(action==="test"){
runTestPrint(bindings[idx]);
}
if(action==="edit-tray"){
openTrayModal(idx);
}
});

// ---------- Tray / printer override modal ----------
const trayModal = document.getElementById("trayModal");
let editingTrayIdx = null;

function updateTrayFieldsVisibility(){
const custom = document.getElementById("traySourceCustom").checked;
document.getElementById("trayCustomFields").style.display = custom ? "flex" : "none";
}

function openTrayModal(idx){
editingTrayIdx = idx;
const b = bindings[idx];
document.getElementById("trayModalTitle").textContent = `紙匣 / 印表機設定 — ${b.cat}`;
document.getElementById("trayModalSub").textContent = `此設定僅套用於「${b.cat}」類別送印時使用`;
document.getElementById("traySourceDefault").checked = b.traySource!=="custom";
document.getElementById("traySourceCustom").checked = b.traySource==="custom";
document.getElementById("trayPrinter").value = b.printer || "";

const presetValues = ["Tray 1","Tray 2","Tray 3","自動選擇","手動送紙"];
const traySelect = document.getElementById("trayTraySelect");
const trayCustomInput = document.getElementById("trayTrayCustom");
if(b.tray && !presetValues.includes(b.tray)){
traySelect.value = "__custom__";
trayCustomInput.value = b.tray;
trayCustomInput.style.display = "block";
} else {
traySelect.value = b.tray || "";
trayCustomInput.value = "";
trayCustomInput.style.display = "none";
}
updateTrayFieldsVisibility();
trayModal.classList.add("show");
}

document.getElementById("traySourceDefault").addEventListener("change", updateTrayFieldsVisibility);
document.getElementById("traySourceCustom").addEventListener("change", updateTrayFieldsVisibility);
document.getElementById("trayTraySelect").addEventListener("change", e=>{
const custom = e.target.value === "__custom__";
document.getElementById("trayTrayCustom").style.display = custom ? "block" : "none";
if(custom) document.getElementById("trayTrayCustom").focus();
});

document.getElementById("cancelTrayBtn").addEventListener("click", ()=> trayModal.classList.remove("show"));

document.getElementById("saveTrayBtn").addEventListener("click", ()=>{
const source = document.getElementById("traySourceCustom").checked ? "custom" : "default";
const b = bindings[editingTrayIdx];
if(source==="default"){
b.traySource = "default";
b.printer = "";
b.tray = "";
} else {
const printer = document.getElementById("trayPrinter").value.trim();
const traySelectVal = document.getElementById("trayTraySelect").value;
const tray = traySelectVal==="__custom__" ? document.getElementById("trayTrayCustom").value.trim() : traySelectVal;
if(!printer || !tray){
showToast("請填寫印表機與紙匣,或改選「沿用 .btw 範本設定」");
return;
}
b.traySource = "custom";
b.printer = printer;
b.tray = tray;
}
trayModal.classList.remove("show");
renderBindings();
showToast(`已更新「${b.cat}」的紙匣設定,請記得儲存設定`);
});

const addRowForm = document.getElementById("addRowForm");
document.getElementById("addBindingBtn").addEventListener("click", ()=>{
populateApiSelect(document.getElementById("newCatApi"), availableApis[0].name);
document.getElementById("newCatCode").value="";
addRowForm.classList.add("show");
});
document.getElementById("cancelAddBtn").addEventListener("click", ()=> addRowForm.classList.remove("show"));
document.getElementById("confirmAddBtn").addEventListener("click", ()=>{
const code = document.getElementById("newCatCode").value.trim().toUpperCase();
const api = document.getElementById("newCatApi").value;
if(!code){ showToast("請輸入工單類別代碼"); return; }
if(bindings.some(b=>b.cat===code)){ showToast(`類別「${code}」已存在`); return; }
bindings.push({cat:code, api, enabled:true, lastTest:null, traySource:"default", printer:"", tray:""});
renderBindings();
addRowForm.classList.remove("show");
showToast(`已新增「${code}」類別綁定,請記得儲存設定`);
});

const actionModal = document.getElementById("actionModal");
const modalLoading = document.getElementById("modalLoading");
const modalResult = document.getElementById("modalResult");

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);
}
}, 280);
}

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"));

function runTestPrint(binding){
const ep = apiByName(binding.api) ? apiByName(binding.api).endpoint : "";
const useCustomTray = binding.traySource==="custom";
const steps = [`呼叫 API:${ep}`, "驗證工單類別參數..."];
if(useCustomTray){
steps.push(`覆寫印表機/紙匣:${binding.printer} · ${binding.tray}`);
}
steps.push("送出測試列印工作...");
runModalSequence({
title:`測試列印 — ${binding.cat}`,
desc:`呼叫「${binding.api}」送出測試標籤`,
steps,
onDone: ()=>{
const success = binding.enabled;
binding.lastTest = { ok: success, time: new Date().toLocaleString('zh-TW',{hour12:false}).slice(0,16).replace(',','') };
showModalResult({
ok: success,
title: success ? "測試列印成功" : "測試列印失敗",
desc: success ? `已成功呼叫「${binding.api}」送出列印` : "此綁定目前為停用狀態,未送出列印",
log: success
? [`✓ API 回應 200 OK`, `✓ 已透過 ${binding.api} 送出`, useCustomTray ? `✓ 紙匣已覆寫為 ${binding.tray}(${binding.printer})` : `✓ 紙匣沿用 .btw 範本設定`]
: [`✕ 綁定狀態為停用,請先啟用後再測試`]
});
renderBindings();
}
});
}

document.getElementById("saveAllBtn").addEventListener("click", ()=>{
runModalSequence({
title:"儲存工單列印設定",
desc:"正在儲存類別 ↔ API 綁定設定",
steps:[
"驗證綁定資料...",
`寫入 ${bindings.length} 筆類別綁定設定...`,
"更新送印呼叫對照表..."
],
onDone: ()=>{
showModalResult({
ok:true,
title:"設定已儲存",
desc:`共 ${bindings.length} 筆工單類別綁定設定已生效`,
log: bindings.map(b=>`✓ ${b.cat} → ${b.api}${b.enabled?"":"(停用)"} · 紙匣:${b.traySource==="custom" ? `${b.tray}(${b.printer})` : "沿用 .btw 範本"}`)
});
document.getElementById("lastSaved").textContent = "上次儲存:" + new Date().toLocaleString('zh-TW',{hour12:true});
}
});
});

function showToast(msg){
const t = document.getElementById("toast");
t.textContent = "✓ " + msg;
t.classList.add("show");
setTimeout(()=>t.classList.remove("show"), 3000);
}

renderBindings();
</script>
</body>
</html>
(1-1/3)