Project

General

Profile

Feature #224 » sv.mes.工單管理_列印工單.v260819.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;
}
*{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 */
.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 */
.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);
}
.content{
padding:20px 28px 40px;
}
.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);
}
.card-head h2{
margin:0;
font-size:16px;
}
.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;
}
.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-print{
background:linear-gradient(135deg,#7c3aed,#6d28d9);
color:#fff;
}
.btn-preview{
background:linear-gradient(135deg,#0d9488,#0f766e);
color:#fff;
}
.row-btn.preview{border-color:#5eead4;color:#0d9488;}
.row-btn.preview:hover{background:#f0fdfa;}

.toolbar{
padding:16px 20px;
display:flex;
align-items:flex-end;
gap:14px;
flex-wrap:wrap;
border-bottom:1px solid var(--gray-200);
background:var(--gray-50);
}
.field{
display:flex;
flex-direction:column;
gap:5px;
}
.field label{
font-size:12px;
color:var(--gray-600);
font-weight:600;
}
.field input[type=text]{
width:280px;
padding:9px 12px;
border:1px solid var(--gray-300);
border-radius:6px;
font-size:13.5px;
}
.field select{
padding:9px 12px;
border:1px solid var(--gray-300);
border-radius:6px;
font-size:13.5px;
background:#fff;
min-width:150px;
}
.field input:focus, .field select:focus{
outline:none;
border-color:var(--blue-500);
box-shadow:0 0 0 3px rgba(59,130,246,.15);
}
.toolbar-actions{
display:flex;
gap:8px;
margin-left:auto;
}

.selection-bar{
display:flex;
align-items:center;
justify-content:space-between;
padding:10px 20px;
background:var(--purple-50);
border-bottom:1px solid #e9d5ff;
font-size:13.5px;
color:#5b21b6;
}
.selection-bar strong{font-size:14px;}
.selection-bar.hidden{display:none;}

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:top;
}
tbody tr:hover{background:var(--gray-50);}
tbody tr.checked{background:#eff6ff;}
.mono{font-family:"SFMono-Regular",Consolas,monospace;color:var(--gray-800);}
.checkbox-cell{width:40px;text-align:center;}
input[type=checkbox]{
width:16px;height:16px;
cursor:pointer;
accent-color:var(--blue-600);
}
.tag{
display:inline-block;
padding:3px 10px;
border-radius:999px;
font-size:11.5px;
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-printed{background:var(--green-50);color:var(--green-600);border:1px solid #bbf7d0;}
.tag-unprinted{background:var(--amber-50);color:var(--amber-600);border:1px solid #fde68a;}
.row-btn{
border:1px solid var(--blue-500);
background:#fff;
color:var(--blue-600);
border-radius:6px;
padding:6px 12px;
font-size:12.5px;
cursor:pointer;
display:inline-flex;
align-items:center;
gap:4px;
}
.row-btn:hover{background:#eff6ff;}
.table-footer{
display:flex;
align-items:center;
justify-content:space-between;
padding:14px 20px;
font-size:13px;
color:var(--gray-500);
}
.pager{display:flex;gap:4px;}
.pager button{
border:1px solid var(--gray-300);
background:#fff;
border-radius:6px;
padding:5px 10px;
font-size:12.5px;
cursor:pointer;
color:var(--gray-600);
}
.pager button:hover:not(:disabled):not(.active){background:var(--gray-50);}
.pager button:disabled{cursor:not-allowed;opacity:.4;}
.pager button.ellipsis{cursor:default;border:none;background:none;}
.pager button.active{
background:var(--blue-600);
border-color:var(--blue-600);
color:#fff;
font-weight:600;
cursor:default;
}
.empty-state{
padding:60px 20px;
text-align:center;
color:var(--gray-500);
font-size:13.5px;
}

/* Modal */
.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 0.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;
}
.warn-circle{
width:52px;height:52px;
border-radius:50%;
background:var(--amber-50);
color:var(--amber-600);
display:flex;align-items:center;justify-content:center;
margin:0 auto 14px;
font-size:28px;
border:1px solid #fde68a;
}
.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%;
}
.modal-list{
text-align:left;
max-height:130px;
overflow-y:auto;
background:var(--gray-50);
border-radius:8px;
padding:10px 14px;
margin:14px 0;
font-size:12.5px;
}
.modal-list div{padding:3px 0;color:var(--gray-600);}
.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;}
</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 current">工單列印</div>
<div class="nav-sub">工單生產紀錄查詢</div>
<div class="nav-sub">生產即時監控看板</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="content">
<div class="card">
<div class="card-head">
<h2>工單列表</h2>
</div>

<div class="toolbar">
<div class="field">
<label>搜尋工單編號</label>
<input type="text" id="searchInput" placeholder="搜尋工單編號...">
</div>
<div class="field">
<label>工單類別</label>
<select id="categoryFilter">
<option value="ALL">全部類別</option>
<option value="OK">OK</option>
<option value="MYOK">MYOK</option>
<option value="RGP">RGP</option>
</select>
</div>
<div class="field">
<label>列印狀態</label>
<select id="printFilter">
<option value="ALL">全部</option>
<option value="UNPRINTED" selected>未列印</option>
<option value="PRINTED">已列印</option>
</select>
</div>
<div class="toolbar-actions">
<button class="btn btn-outline" id="searchBtn">搜尋</button>
<button class="btn btn-gray" id="clearBtn">清空</button>
</div>
</div>

<div class="selection-bar hidden" id="selectionBar">
<span>已選取 <strong id="selCount">0</strong> 筆工單</span>
<div style="display:flex;gap:8px;">
<button class="btn btn-preview" id="previewSelectedBtn">🔍 預覽 PDF(不送印)</button>
<button class="btn btn-print" id="printBtn">🖨️ 列印工單</button>
</div>
</div>

<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th class="checkbox-cell"><input type="checkbox" id="selectAll"></th>
<th>工單編號</th>
<th>客戶名稱</th>
<th>料號</th>
<th>顏色</th>
<th>雷刻1</th>
<th>雷刻2</th>
<th>LotNo</th>
<th>工單類別</th>
<th>備註</th>
<th>建立日期</th>
<th>修改日期</th>
<th>列印狀態</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div id="emptyState" class="empty-state" style="display:none;">找不到符合條件的工單</div>

<div class="table-footer">
<div>共 <span id="totalCount">0</span> 筆資料,第 <span id="pageInfo">1</span> 頁</div>
<div class="pager" id="pager"></div>
</div>
</div>
</div>
</div>

<!-- Mix Status Warning Modal -->
<div class="modal-overlay" id="mixWarningModal">
<div class="modal">
<div class="warn-circle">⚠️</div>
<h3>列印狀態警告</h3>
<p id="mixWarningMsg" style="margin-top:8px; line-height:1.5; color:var(--gray-800);"></p>
<p style="margin-top:12px; font-weight:600;">請問是否繼續執行此次列印?</p>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-gray" style="flex:1;" id="cancelMixBtn">取消</button>
<button class="btn btn-print" style="flex:1;" id="confirmMixBtn">確認列印</button>
</div>
</div>
</div>

<!-- Print Modal -->
<div class="modal-overlay" id="printModal">
<div class="modal">
<div id="modalPhaseConfirm">
<h3>確認列印工單</h3>
<p>即將送出以下工單至 BarTender Server 列印</p>
<div class="modal-list" id="confirmList"></div>
<div style="display:flex;gap:10px;margin-top:8px;">
<button class="btn btn-gray" style="flex:1;" id="cancelPrintBtn">取消</button>
<button class="btn btn-print" style="flex:1;" id="confirmPrintBtn">確認送印</button>
</div>
</div>

<div id="modalPhaseLoading" style="display:none;">
<h3>正在送出列印工作</h3>
<p>連接 BarTender Server 中,請稍候…</p>
<div class="spinner"></div>
<div class="modal-log" id="progressLog"></div>
</div>

<div id="modalPhaseDone" style="display:none;">
<div class="check-circle">✓</div>
<h3>列印工作已送出</h3>
<p id="doneSummary"></p>
<div class="modal-log" id="doneLog"></div>
<button class="btn btn-blue modal-close" id="closeModalBtn">完成</button>
</div>
</div>
</div>

<!-- Preview (PDF, no printer bound) Modal -->
<div class="modal-overlay" id="previewModal">
<div class="modal">
<div id="previewPhaseConfirm">
<h3>預覽工單標籤(PDF)</h3>
<p>將呼叫與正式列印相同的 BarTender API,但本次呼叫不綁定實體印表機,改以 PDF 虛擬印表機輸出,僅產生預覽檔案,不會實際送印、也不會標記為已列印</p>
<div class="modal-list" id="previewConfirmList"></div>
<div style="display:flex;gap:10px;margin-top:8px;">
<button class="btn btn-gray" style="flex:1;" id="cancelPreviewBtn">取消</button>
<button class="btn btn-preview" style="flex:1;" id="confirmPreviewBtn">產生 PDF 預覽</button>
</div>
</div>

<div id="previewPhaseLoading" style="display:none;">
<h3>正在產生 PDF 預覽</h3>
<p>連接 BarTender Server 中,請稍候…</p>
<div class="spinner"></div>
<div class="modal-log" id="previewProgressLog"></div>
</div>

<div id="previewPhaseDone" style="display:none;">
<div class="check-circle">✓</div>
<h3>PDF 預覽已產生</h3>
<p id="previewDoneSummary"></p>
<div class="modal-log" id="previewDoneLog"></div>
<div style="display:flex;gap:10px;margin-top:8px;">
<button class="btn btn-preview" style="flex:1;" id="openPdfBtn">📄 開啟 PDF 預覽</button>
<button class="btn btn-gray modal-close" style="flex:1;" id="closePreviewModalBtn">關閉</button>
</div>
</div>
</div>
</div>

<div class="toast" id="toast"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script>
const customers = ["大學台中西屯","卡爾蔡司(广州)","BRIGHTEN OPTIX (MALAYSIA) SDN. BHD.","諾貝爾嘉義朴子","精華光學股份有限公司","晶碩光學","視鼎光電","科妍生技"];
const colors = ["紫色(Violet)","綠色(Green)","冰藍(Ice Blue)","藍色(Blue)","琥珀(Amber)","灰色(Grey)"];
const categories = ["OK","MYOK","RGP"];
const prefixes = ["A511","A512","A513"];

function pad(n){return n.toString().padStart(4,"0");}

function genData(n){
const rows=[];
for(let i=1;i<=n;i++){
const prefix = prefixes[i%prefixes.length];
const cat = categories[i%categories.length];
const printed = i%3===0; // 部分示範資料預設已列印
rows.push({
id: `${prefix}-2606${pad(220000+i).slice(2)}`,
customer: customers[i%customers.length],
partNo: `F${cat[0]}${(1000+i*37)%9999}`.toUpperCase(),
color: colors[i%colors.length],
laser1: (6149000+i*13).toString(),
laser2: i%3===0 ? "ZEISS" : "Logo.dxf",
lot: (25300000+i*7).toString().slice(0,8),
category: cat,
note: i%4===0 ? "OZ6.0" : (i%5===0 ? "EF85-032" : ""),
created: "2026/6/22 下午 02:39:5" + (i%9),
modified: "2026/6/22 下午 04:42:25",
printed: printed,
printedAt: printed ? "2026/6/23 上午 09:1" + (i%9) : ""
});
}
return rows;
}

const allData = genData(24);
let filtered = allData.slice();
const selected = new Set();
const pageSize = 10;
let currentPage = 1;

const tagClass = {OK:"tag-ok",MYOK:"tag-myok",RGP:"tag-rgp"};

function render(){
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
if(currentPage > totalPages) currentPage = totalPages;
if(currentPage < 1) currentPage = 1;
const pagedRows = filtered.slice((currentPage-1)*pageSize, currentPage*pageSize);

const body = document.getElementById("tableBody");
const empty = document.getElementById("emptyState");
body.innerHTML = "";
if(filtered.length===0){
empty.style.display="block";
} else {
empty.style.display="none";
pagedRows.forEach(row=>{
const tr = document.createElement("tr");
tr.dataset.id = row.id;
if(selected.has(row.id)) tr.classList.add("checked");
tr.innerHTML = `
<td class="checkbox-cell"><input type="checkbox" class="rowCheck" data-id="${row.id}" ${selected.has(row.id)?"checked":""}></td>
<td class="mono">${row.id}</td>
<td>${row.customer}</td>
<td class="mono">${row.partNo}</td>
<td>${row.color}</td>
<td class="mono">${row.laser1}</td>
<td>${row.laser2}</td>
<td class="mono">${row.lot}</td>
<td><span class="tag ${tagClass[row.category]}">${row.category}</span></td>
<td>${row.note}</td>
<td>${row.created}</td>
<td>${row.modified}</td>
<td>
<span class="tag ${row.printed?"tag-printed":"tag-unprinted"}">${row.printed?"已列印":"未列印"}</span>
${row.printed?`<div style="font-size:11px;color:var(--gray-500);margin-top:3px;">${row.printedAt}</div>`:""}
</td>
<td>
<div style="display:flex;gap:6px;">
<button class="row-btn preview" data-action="preview-row" data-id="${row.id}">🔍 預覽</button>

</div>
</td>
`;
body.appendChild(tr);
});
}
document.getElementById("totalCount").textContent = filtered.length;
document.getElementById("pageInfo").textContent = `${currentPage} / ${totalPages}`;
renderPager(totalPages);
updateSelectAllState();
updateSelectionBar();
}

function renderPager(totalPages){
const pager = document.getElementById("pager");
pager.innerHTML = "";

const makeBtn = (label, page, opts={})=>{
const btn = document.createElement("button");
btn.textContent = label;
if(opts.active) btn.classList.add("active");
if(opts.ellipsis){ btn.classList.add("ellipsis"); btn.disabled = true; }
if(opts.disabled) btn.disabled = true;
if(!opts.ellipsis && !opts.disabled){
btn.addEventListener("click", ()=>{
currentPage = page;
render();
});
}
return btn;
};

pager.appendChild(makeBtn("上一頁", currentPage-1, {disabled: currentPage<=1}));

const pages = [];
const addPage = p => { if(p>=1 && p<=totalPages && !pages.includes(p)) pages.push(p); };
addPage(1);
addPage(2);
for(let p=currentPage-1; p<=currentPage+1; p++) addPage(p);
addPage(totalPages-1);
addPage(totalPages);
pages.sort((a,b)=>a-b);

let prev = 0;
pages.forEach(p=>{
if(p - prev > 1) pager.appendChild(makeBtn("…", null, {ellipsis:true}));
pager.appendChild(makeBtn(String(p), p, {active: p===currentPage}));
prev = p;
});

pager.appendChild(makeBtn("下一頁", currentPage+1, {disabled: currentPage>=totalPages}));
}

function updateSelectAllState(){
// 全選涵蓋「目前篩選條件下的所有資料」,不限本頁,這樣切換分頁後仍能反映實際勾選狀態
const all = document.getElementById("selectAll");
const visibleIds = filtered.map(r=>r.id);
const allChecked = visibleIds.length>0 && visibleIds.every(id=>selected.has(id));
all.checked = allChecked;
all.indeterminate = !allChecked && visibleIds.some(id=>selected.has(id));
}

function updateSelectionBar(){
const bar = document.getElementById("selectionBar");
document.getElementById("selCount").textContent = selected.size;
if(selected.size>0){
bar.classList.remove("hidden");
} else {
bar.classList.add("hidden");
}
}

function applyFilters(){
const kw = document.getElementById("searchInput").value.trim().toLowerCase();
const cat = document.getElementById("categoryFilter").value;
const printStatus = document.getElementById("printFilter").value;
filtered = allData.filter(r=>{
const matchKw = !kw || r.id.toLowerCase().includes(kw);
const matchCat = cat==="ALL" || r.category===cat;
const matchPrint = printStatus==="ALL" || (printStatus==="PRINTED" ? r.printed : !r.printed);
return matchKw && matchCat && matchPrint;
});
currentPage = 1;
render();
}

document.getElementById("searchBtn").addEventListener("click", applyFilters);
document.getElementById("categoryFilter").addEventListener("change", applyFilters);
document.getElementById("printFilter").addEventListener("change", applyFilters);
document.getElementById("searchInput").addEventListener("keydown", e=>{ if(e.key==="Enter") applyFilters(); });
document.getElementById("clearBtn").addEventListener("click", ()=>{
document.getElementById("searchInput").value="";
document.getElementById("categoryFilter").value="ALL";
document.getElementById("printFilter").value="ALL";
applyFilters();
});

document.getElementById("tableBody").addEventListener("change", e=>{
if(e.target.classList.contains("rowCheck")){
const id = e.target.dataset.id;
if(e.target.checked) selected.add(id); else selected.delete(id);
e.target.closest("tr").classList.toggle("checked", e.target.checked);
updateSelectAllState();
updateSelectionBar();
}
});

document.getElementById("selectAll").addEventListener("change", e=>{
// 即使資料分成多頁,全選/取消全選也會套用到目前篩選條件下的「全部」資料,不只本頁
filtered.forEach(r=>{
if(e.target.checked) selected.add(r.id); else selected.delete(r.id);
});
render();
});

document.getElementById("tableBody").addEventListener("click", e=>{
const btn = e.target.closest('button[data-action="preview-row"]');
if(!btn) return;
openPreviewModal([btn.dataset.id]);
});

// Print flow
const printModal = document.getElementById("printModal");
const mixWarningModal = document.getElementById("mixWarningModal");
const phaseConfirm = document.getElementById("modalPhaseConfirm");
const phaseLoading = document.getElementById("modalPhaseLoading");
const phaseDone = document.getElementById("modalPhaseDone");

// 檢查並觸發列印行為
function handlePrintTrigger(){
if(selected.size===0) return;
const selectedRows = [...selected].map(id => allData.find(r => r.id === id));
const hasUnprinted = selectedRows.some(r => !r.printed);
const hasPrinted = selectedRows.some(r => r.printed);

// 如果選取的工單同時包含「未列印」與「已列印」
if(hasUnprinted && hasPrinted){
const unprintedCount = selectedRows.filter(r => !r.printed).length;
const printedCount = selectedRows.filter(r => r.printed).length;
document.getElementById("mixWarningMsg").innerHTML =
`您選取的 <strong>${selected.size}</strong> 筆工單中,包含:<br>` +
`<span style="color:var(--amber-600);font-weight:600;">未列印:${unprintedCount} 筆</span> 與 ` +
`<span style="color:var(--green-600);font-weight:600;">已列印:${printedCount} 筆</span>。`;
mixWarningModal.classList.add("show");
} else {
openPrintModal();
}
}

function openPrintModal(){
if(selected.size===0) return;
phaseConfirm.style.display="block";
phaseLoading.style.display="none";
phaseDone.style.display="none";
const list = document.getElementById("confirmList");
list.innerHTML = [...selected].map(id=>{
const row = allData.find(r=>r.id===id);
return `<div>▸ ${id} <span class="tag ${tagClass[row.category]}" style="margin-left:6px;">${row.category}</span></div>`;
}).join("");
printModal.classList.add("show");
}

let lastPrintedIds = [];

document.getElementById("printBtn").addEventListener("click", handlePrintTrigger);

// 警告 Dialog 的按鈕事件
document.getElementById("cancelMixBtn").addEventListener("click", ()=>{
mixWarningModal.classList.remove("show");
});

document.getElementById("confirmMixBtn").addEventListener("click", ()=>{
mixWarningModal.classList.remove("show");
openPrintModal();
});

document.getElementById("cancelPrintBtn").addEventListener("click", ()=>printModal.classList.remove("show"));
document.getElementById("closeModalBtn").addEventListener("click", ()=>{
printModal.classList.remove("show");
showToast(`已送出 ${lastPrintedIds.length} 張工單至印表機`);
});

document.getElementById("confirmPrintBtn").addEventListener("click", ()=>{
phaseConfirm.style.display="none";
phaseLoading.style.display="block";
const ids = [...selected];
lastPrintedIds = ids;
const log = document.getElementById("progressLog");
log.innerHTML = "";
const steps = [
"連接 BarTender Server (127.0.0.1:6608)...",
"驗證列印範本 WorkOrder_Label.btw...",
`建立列印批次,共 ${ids.length} 筆工單...`,
...ids.map(id=>`送出工單 ${id} 至列印佇列...`),
"等待印表機回應...",
];
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(()=>{
phaseLoading.style.display="none";
phaseDone.style.display="block";
document.getElementById("doneSummary").textContent = `共 ${ids.length} 筆工單已成功送出至印表機佇列`;
const doneLog = document.getElementById("doneLog");
doneLog.innerHTML = ids.map(id=>`<div>✓ ${id} — 列印成功</div>`).join("");
// 標記為已列印,並依目前篩選條件重新整理(預設篩選為「未列印」,列印完成後會自動從清單移除)
const now = new Date().toLocaleString('zh-TW',{hour12:true});
ids.forEach(id=>{
const row = allData.find(r=>r.id===id);
if(row){ row.printed = true; row.printedAt = now; }
});
selected.clear();
applyFilters();
}, 500);
}
}, 260);
});

// ---------- Preview flow (呼叫同一支 BarTender API,但改用 PDF 虛擬印表機輸出,不送印、不標記已列印) ----------
const previewModal = document.getElementById("previewModal");
const previewPhaseConfirm = document.getElementById("previewPhaseConfirm");
const previewPhaseLoading = document.getElementById("previewPhaseLoading");
const previewPhaseDone = document.getElementById("previewPhaseDone");
let previewIds = [];

function openPreviewModal(ids){
if(!ids || ids.length===0) return;
previewIds = ids;
previewPhaseConfirm.style.display="block";
previewPhaseLoading.style.display="none";
previewPhaseDone.style.display="none";
const list = document.getElementById("previewConfirmList");
list.innerHTML = ids.map(id=>{
const row = allData.find(r=>r.id===id);
return `<div>▸ ${id} <span class="tag ${tagClass[row.category]}" style="margin-left:6px;">${row.category}</span></div>`;
}).join("");
previewModal.classList.add("show");
}

document.getElementById("previewSelectedBtn").addEventListener("click", ()=> openPreviewModal([...selected]));
document.getElementById("cancelPreviewBtn").addEventListener("click", ()=> previewModal.classList.remove("show"));
document.getElementById("closePreviewModalBtn").addEventListener("click", ()=> previewModal.classList.remove("show"));

document.getElementById("confirmPreviewBtn").addEventListener("click", ()=>{
previewPhaseConfirm.style.display="none";
previewPhaseLoading.style.display="block";
const ids = previewIds;
const log = document.getElementById("previewProgressLog");
log.innerHTML = "";
const steps = [
"連接 BarTender Server (127.0.0.1:6608)...",
"驗證列印範本 WorkOrder_Label.btw...",
`建立列印批次,共 ${ids.length} 筆工單...`,
"本次呼叫未指定實體印表機,改綁定 PDF 虛擬印表機...",
...ids.map(id=>`轉出工單 ${id} 為 PDF 頁面...`),
"組合為單一 PDF 檔案...",
];
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(()=>{
previewPhaseLoading.style.display="none";
previewPhaseDone.style.display="block";
document.getElementById("previewDoneSummary").textContent = `共 ${ids.length} 筆工單已轉出為 PDF 預覽檔(未送印、未標記已列印)`;
const doneLog = document.getElementById("previewDoneLog");
doneLog.innerHTML = ids.map(id=>`<div>✓ ${id} — PDF 頁面已產生</div>`).join("");
}, 500);
}
}, 260);
});

document.getElementById("openPdfBtn").addEventListener("click", ()=>{
buildLabelPdf(previewIds);
});

// 產生實際可開啟/下載的 PDF,用來模擬「BarTender 以 PDF 虛擬印表機輸出」的預覽結果
function buildLabelPdf(ids){
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ unit:"mm", format:[100,70] }); // 模擬 100x70mm 標籤尺寸

ids.forEach((id, idx)=>{
if(idx>0) doc.addPage([100,70]);
const row = allData.find(r=>r.id===id);
if(!row) return;

doc.setDrawColor(180);
doc.rect(3,3,94,64);

doc.setFontSize(6);
doc.setTextColor(140);
doc.text("PDF 預覽(未實際送印 · WorkOrder_Label.btw)", 6, 8);

doc.setFontSize(13);
doc.setTextColor(20);
doc.setFont(undefined, "bold");
doc.text(row.id, 6, 17);

doc.setFont(undefined, "normal");
doc.setFontSize(9);
doc.text(`客戶:${row.customer}`, 6, 24);
doc.text(`料號:${row.partNo} 顏色:${row.color}`, 6, 30);
doc.text(`雷刻1:${row.laser1}`, 6, 36);
doc.text(`雷刻2:${row.laser2}`, 6, 41);
doc.text(`LotNo:${row.lot}`, 6, 46);
if(row.note) doc.text(`備註:${row.note}`, 6, 51);

doc.setFontSize(10);
doc.setFont(undefined, "bold");
doc.text(`[${row.category}]`, 84, 17, { align:"right" });

// 簡易條碼示意(純視覺,非真實可掃描條碼)
doc.setFont(undefined, "normal");
let x = 6;
const barY = 56, barH = 8;
for(let i=0;i<row.id.length*2;i++){
const w = (i%3===0) ? 0.8 : 0.4;
doc.setFillColor(30);
doc.rect(x, barY, w, barH, "F");
x += w + 0.5;
if(x>90) break;
}
doc.setFontSize(6);
doc.setTextColor(140);
doc.text(row.id, 6, barY+barH+3);
});

doc.output("dataurlnewwindow", { filename: `WorkOrder_Preview_${ids.length}pcs.pdf` });
}

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

applyFilters();
</script>
</body>
</html>
(2-2/3)