Project

General

Profile

Feature #221 » sv.mes.加工參數檔設定(xml).html

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

 
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>MES系統 - 加工參數檔設定</title>
<style>
:root{
--blue-900:#1e3a8a; --blue-800:#1e40af; --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:16px;}

.cat-tabs{display:flex;gap:8px;flex-wrap:wrap;}
.cat-tab{
padding:9px 16px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;
background:#fff;border:1px solid var(--gray-300);color:var(--gray-600);
display:flex;align-items:center;gap:8px;
}
.cat-tab .count{background:var(--gray-100);color:var(--gray-500);border-radius:999px;padding:1px 8px;font-size:11.5px;font-weight:700;}
.cat-tab.active{background:var(--blue-600);border-color:var(--blue-600);color:#fff;}
.cat-tab.active .count{background:rgba(255,255,255,.25);color:#fff;}
.cat-tab .fname{font-family:Consolas,monospace;font-weight:400;opacity:.8;font-size:11.5px;}
.cat-tab.add-tab{border-style:dashed;color:var(--blue-600);font-weight:700;}
.cat-tab.add-tab:hover{background:#eff6ff;border-color:var(--blue-500);}

.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;display:flex;align-items:center;gap:8px;}
.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;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-outline{background:#fff;color:var(--blue-600);border:1px solid var(--blue-500);}
.btn-purple{background:linear-gradient(135deg,#7c3aed,#6d28d9);color:#fff;}
.btn-danger-outline{background:#fff;color:var(--red-600);border:1px solid #fecaca;}
.btn-sm{padding:6px 12px;font-size:12.5px;}

table{width:100%;border-collapse:collapse;min-width:1080px;}
thead th{text-align:left;font-size:12px;color:var(--gray-500);font-weight:600;padding:10px 10px;border-bottom:1px solid var(--gray-200);white-space:nowrap;background:var(--gray-50);position:sticky;top:0;}
tbody td{padding:6px 10px;border-bottom:1px solid var(--gray-100);font-size:13px;vertical-align:middle;}
tbody tr:hover{background:var(--gray-50);}
tbody tr.dup td{background:var(--red-50);}

.order-num{color:var(--gray-500);font-size:12.5px;font-family:Consolas,monospace;padding-left:6px;}

.cell-input, .cell-select{
padding:7px 9px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;background:#fff;width:100%;
font-family:Consolas,monospace;
}
.cell-input.wide{min-width:150px;}
.cell-input.small{width:64px;}
.cell-select{font-family:inherit;min-width:150px;}
.cell-input:disabled, .cell-select:disabled{background:var(--gray-100);color:var(--gray-400,#94a3b8);cursor:not-allowed;}
.cell-input:focus, .cell-select:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
.cell-input.error{border-color:var(--red-600);background:var(--red-50);}

.type-badge{display:inline-block;font-size:10.5px;font-weight:700;padding:2px 6px;border-radius:4px;margin-top:4px;}
.type-1{background:var(--green-50);color:var(--green-600);}
.type-3{background:var(--amber-50);color:var(--amber-600);}
.type-4{background:var(--purple-50);color:var(--purple-600);}

.icon-btn{border:1px solid var(--gray-300);background:#fff;color:var(--red-600);border-radius:6px;padding:6px 9px;font-size:12px;cursor:pointer;}
.icon-btn:hover{background:var(--red-50);border-color:#fecaca;}
.icon-btn.plain{color:var(--gray-600);}
.icon-btn.plain:hover{background:var(--gray-50);border-color:var(--gray-300);}
.icon-btn:disabled{opacity:.35;cursor:not-allowed;}

.match-rule-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:12px 20px;margin:0 20px 0;background:#f5f3ff;border:1px solid #ddd6fe;border-radius:8px;color:#5b21b6;font-size:12.5px;}
.match-rule-row b{color:#5b21b6;}

.modal.rule-modal{width:920px;max-width:96vw;text-align:left;padding:0;display:flex;flex-direction:column;max-height:88vh;}
.rule-modal-head{padding:20px 24px;border-bottom:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:flex-start;gap:12px;}
.rule-modal-head h3{margin:0 0 4px;font-size:16px;}
.rule-modal-head p{margin:0;font-size:12.5px;color:var(--gray-500);}
.rule-modal-body{padding:18px 24px;overflow-y:auto;flex:1;}
.rule-modal-foot{padding:16px 24px;border-top:1px solid var(--gray-200);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;}

.rule-table{width:100%;border-collapse:collapse;}
.rule-table th{text-align:left;font-size:11.5px;color:var(--gray-500);font-weight:600;padding:8px 8px;border-bottom:1px solid var(--gray-200);white-space:nowrap;}
.rule-table td{padding:6px 8px;border-bottom:1px solid var(--gray-100);vertical-align:middle;}
.rule-input{width:100%;padding:7px 9px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;font-family:Consolas,monospace;background:#fff;}
.rule-input.wide{min-width:160px;}
.rule-input:focus{outline:none;border-color:var(--blue-500);box-shadow:0 0 0 3px rgba(59,130,246,.15);}
.rule-input.invalid{border-color:var(--red-600);background:var(--red-50);}
.rule-order{font-family:Consolas,monospace;color:var(--gray-500);font-size:12px;width:20px;text-align:center;}
.regex-error{font-size:10.5px;color:var(--red-600);margin-top:2px;}

.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;flex-wrap:wrap;}

.test-panel{margin-top:18px;background:var(--gray-50);border:1px solid var(--gray-200);border-radius:8px;padding:14px 16px;}
.test-panel h4{margin:0 0 10px;font-size:13px;}
.test-grid{display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end;}
.test-grid .field label{font-size:11.5px;}
.test-grid input{padding:9px 10px;border:1px solid var(--gray-300);border-radius:6px;font-size:12.5px;font-family:Consolas,monospace;}
.test-result{margin-top:12px;font-size:12.5px;padding:10px 12px;border-radius:8px;line-height:1.6;}
.test-result.hit{background:var(--green-50);color:#166534;border:1px solid #bbf7d0;}
.test-result.miss{background:var(--red-50);color:#991b1b;border:1px solid #fecaca;}
.test-result code{background:rgba(0,0,0,.06);padding:1px 5px;border-radius:4px;}

.toolbar-row{display:flex;align-items:center;justify-content:space-between;padding:12px 20px;border-bottom:1px solid var(--gray-100);gap:12px;flex-wrap:wrap;}
.toolbar-row .left{display:flex;align-items:center;gap:10px;font-size:12.5px;color:var(--gray-500);}

.ftps-btn{
display:inline-flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--gray-300);border-radius:8px;
padding:7px 12px;cursor:pointer;font-size:12.5px;color:var(--gray-600);
}
.ftps-btn:hover{border-color:var(--blue-500);background:#f8fbff;}
.ftps-btn .ftps-dest{font-family:Consolas,monospace;font-size:11.5px;color:var(--gray-500);}
.ftps-btn .ftps-dest.unset{color:var(--amber-600);}
.ftps-btn .ftps-icon{font-size:14px;}

.field{display:flex;flex-direction:column;gap:5px;}
.field.full{grid-column:1 / -1;}
.field label{font-size:12.5px;color:var(--gray-600);font-weight:600;}
.field input[type=text],.field input[type=password],.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);}
.pw-wrap{position:relative;}
.pw-wrap input{padding-right:52px !important;}
.pw-wrap button{position:absolute;right:8px;top:50%;transform:translateY(-50%);border:none;background:none;color:var(--blue-600);cursor:pointer;font-size:12px;font-weight:600;}
.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px 16px;}

.alert{display:flex;gap:10px;align-items:flex-start;padding:12px 16px;border-radius:8px;font-size:12.5px;line-height:1.6;margin:14px 20px 0;}
.alert-error{background:var(--red-50);border:1px solid #fecaca;color:#991b1b;}
.alert-info{background:#eff6ff;border:1px solid #bfdbfe;color:#1e40af;}
.alert .ico{font-size:15px;}

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

.legend{display:flex;gap:16px;flex-wrap:wrap;padding:10px 20px 0;font-size:12px;color:var(--gray-500);}
.legend span{display:inline-flex;align-items:center;gap:6px;}
.legend .dot{width:9px;height:9px;border-radius:2px;}
.dot-1{background:var(--green-600);}
.dot-3{background:var(--amber-600);}
.dot-4{background:var(--purple-600);}

.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.wide{width:640px;text-align:left;}
.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;}
.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%;}
.xml-pre{background:#0f172a;color:#a5f3fc;border-radius:8px;padding:16px;font-size:12px;font-family:Consolas,monospace;line-height:1.6;max-height:420px;overflow:auto;white-space:pre;}

.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 active">設備加工檔</div>
<div class="nav-sub">雷刻檔設定(.txt)</div>
<div class="nav-sub current">加工參數檔設定(.xml)</div>
<div class="nav-sub">加工參數檔設定(.dat)</div>
<div class="nav-item">品質管理</div>
<div class="nav-item">異常管理</div>
<div class="nav-item">課程模組</div>
<div class="nav-item">製造執行中心</div>
<div class="nav-sub">工單管理</div>
<div class="nav-sub">工單生產紀錄查詢</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 class="breadcrumb">設備加工檔 / 加工參數檔設定(.xml)</div>
</div>

<div class="content">

<div class="cat-tabs" id="catTabs"></div>

<div class="card">
<div class="card-head">
<div>
<h2 id="catTitle">—</h2>
<p>每一列的主鍵為「處方規格代碼」,不可重複;編輯後可轉換為 .xml 範本並存至 Server</p>
</div>
<div class="actions">
<button class="btn btn-blue btn-sm" id="editModeBtn">✎ 修改</button>
<button class="btn btn-gray btn-sm" id="cancelEditBtn" style="display:none;">✕ 取消</button>
<button class="btn btn-outline btn-sm" id="addRowBtn" style="display:none;">+ 新增列</button>
<button class="btn btn-danger-outline btn-sm" id="deleteCatBtn" style="display:none;">🗑 刪除此類別</button>
<button class="btn btn-purple btn-sm" id="previewXmlBtn">🔍 預覽 XML</button>
<button class="btn btn-green btn-sm" id="saveXmlBtn" style="display:none;">💾儲存</button>
</div>
</div>
<div class="alert alert-info" id="lockedHint" style="margin:0 20px 0;">
<span class="ico">🔒</span>
<div>目前為檢視模式,欄位不可編輯,「新增列」與「儲存」按鈕也會暫時隱藏。請點「✎ 修改」解鎖後即可編輯,並會出現「儲存」按鈕。</div>
</div>

<div class="match-rule-row" id="matchRuleRow" style="display:none;">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:16px;">🧪</span>
<span>類別自動比對規則(Regular Expression)— 依 <b>客戶</b> / <b>產品設計碼</b> / <b>產品類別</b> 判斷是否套用此範本:目前 <b id="matchRuleCount">0</b> 條規則</span>
</div>
<button class="btn btn-outline btn-sm" id="editMatchRulesBtn">📐 設定比對規則</button>
</div>

<div class="legend">
<span><span class="dot dot-1"></span>1:處方規格內容 — 需填處方規格代碼</span>
<span><span class="dot dot-3"></span>3:固定值 — 需填固定值</span>
<span><span class="dot dot-4"></span>4:處方規格運算 — 需填運算代碼1 / 運算方式 / 運算代碼2</span>
</div>

<div id="dupAlert" style="display:none;"></div>

<div class="toolbar-row">
<div class="left">目前檔案:<span id="fileNameLabel" style="font-family:Consolas,monospace;"></span></div>
<div class="left">共 <b id="rowCount">0</b> 列</div>
<button class="ftps-btn" id="ftpsBtn">
<span class="ftps-icon">🔌</span>
<span>FTPS 設定:</span>
<span class="ftps-dest" id="ftpsDestLabel">未設定</span>
</button>
</div>

<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th style="width:36px;">#</th>
<th style="width:70px;">Par code</th>
<th style="width:60px;">zone</th>
<th style="min-width:170px;">資料類型</th>
<th style="min-width:160px;">處方規格代碼(主鍵)</th>
<th style="width:80px;">固定值</th>
<th style="min-width:130px;">運算代碼1</th>
<th style="min-width:110px;">運算方式</th>
<th style="min-width:130px;">運算代碼2</th>
<th style="width:44px;"></th>
</tr>
</thead>
<tbody id="rowsBody"></tbody>
</table>
</div>

<div class="card-foot">
<span class="last-saved" id="lastSaved">尚未儲存</span>
<span class="last-saved">上傳目的地(FTPS):<span id="serverPathLabel" style="font-family:Consolas,monospace;"></span></span>
</div>
</div>

</div>
</div>

<datalist id="specCodes"></datalist>

<!-- XML preview modal -->
<div class="modal-overlay" id="xmlModal">
<div class="modal wide">
<h3 id="xmlModalTitle">XML 預覽</h3>
<p id="xmlModalSub"></p>
<div class="xml-pre" id="xmlPreviewContent" style="margin-top:14px;"></div>
<button class="btn btn-gray modal-close" id="closeXmlBtn">關閉</button>
</div>
</div>

<!-- Match rule editor modal (Regex: 客戶/產品設計碼/產品類別) -->
<div class="modal-overlay" id="matchRuleModal">
<div class="modal rule-modal">
<div class="rule-modal-head">
<div>
<h3 id="matchRuleModalTitle">類別自動比對規則</h3>
<p id="matchRuleModalSub"></p>
</div>
<button class="icon-btn plain" id="closeMatchRuleModalBtn">✕ 關閉</button>
</div>
<div class="rule-modal-body">
<div class="alert alert-info" style="margin:0 0 14px;">
<span class="ico">ℹ️</span>
<div>規則由上而下依序比對,「客戶」「產品設計碼」「產品類別」三個條件皆符合(欄位留空視為不限)的第一條規則即命中,系統將自動套用本類別作為加工參數範本。</div>
</div>

<table class="rule-table">
<thead>
<tr>
<th>順序</th>
<th style="min-width:160px;">客戶 (Regex)</th>
<th style="min-width:160px;">產品設計碼 (Regex)</th>
<th style="min-width:160px;">產品類別 (Regex)</th>
<th style="min-width:140px;">說明</th>
<th style="width:56px;">啟用</th>
<th style="width:110px;">排序</th>
<th style="width:36px;"></th>
</tr>
</thead>
<tbody id="matchRuleTableBody"></tbody>
</table>

<button class="btn btn-outline btn-sm" id="addMatchRuleBtn" style="margin-top:12px;">+ 新增規則</button>

<div class="test-panel">
<h4>🧪 測試比對</h4>
<div class="test-grid">
<div class="field"><label>客戶</label><input type="text" id="mrTestCustomer" placeholder="例如 HOYA"></div>
<div class="field"><label>產品設計碼</label><input type="text" id="mrTestDesignCode" placeholder="例如 FK203"></div>
<div class="field"><label>產品類別</label><input type="text" id="mrTestProdCategory" placeholder="例如 一般片"></div>
<button class="btn btn-purple btn-sm" id="runMatchTestBtn">執行測試</button>
</div>
<div id="mrTestResult"></div>
</div>
</div>
<div class="rule-modal-foot">
<span style="font-size:12px;color:var(--gray-500);" id="matchRuleCountLabel"></span>
<div style="display:flex;gap:8px;">
<button class="btn btn-gray btn-sm" id="cancelMatchRuleBtn">取消</button>
<button class="btn btn-green btn-sm" id="applyMatchRuleBtn">套用規則</button>
</div>
</div>
</div>
</div>

<!-- Confirm modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal">
<h3 id="confirmTitle">確認刪除</h3>
<p id="confirmDesc"></p>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-gray" style="flex:1;" id="confirmCancelBtn">取消</button>
<button class="btn" style="flex:1;background:var(--red-600);color:#fff;" id="confirmOkBtn">刪除</button>
</div>
</div>
</div>

<!-- Add category modal -->
<div class="modal-overlay" id="addCatModal">
<div class="modal">
<h3>新增類別</h3>
<p>建立一個全新、空白的加工參數類別,可自行編輯列與內容後儲存</p>
<div style="text-align:left;margin-top:16px;display:flex;flex-direction:column;gap:14px;">
<div class="field">
<label>類別代碼</label>
<input type="text" id="newCatKey" placeholder="例如 OK_myOK_Pro">
</div>
<div class="field">
<label>檔名 (.xml)</label>
<input type="text" id="newCatFile" placeholder="例如 OK_myOK_Pro.xml">
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-gray" style="flex:1;" id="cancelAddCatBtn">取消</button>
<button class="btn btn-green" style="flex:1;" id="confirmAddCatBtn">建立</button>
</div>
</div>
</div>

<!-- FTPS settings modal -->
<div class="modal-overlay" id="ftpsModal">
<div class="modal wide">
<h3 id="ftpsModalTitle">FTPS 連線設定</h3>
<p id="ftpsModalSub"></p>
<div class="form-grid" style="text-align:left;margin-top:16px;">
<div class="field">
<label>Host</label>
<input type="text" id="ftpsHost" placeholder="ftps.example.com">
</div>
<div class="field">
<label>Port</label>
<input type="text" id="ftpsPort" placeholder="21">
</div>
<div class="field">
<label>帳號</label>
<input type="text" id="ftpsUser" placeholder="帳號">
</div>
<div class="field">
<label>密碼</label>
<div class="pw-wrap">
<input type="password" id="ftpsPass" placeholder="密碼">
<button type="button" id="toggleFtpsPw">顯示</button>
</div>
</div>
<div class="field full">
<label>遠端路徑</label>
<input type="text" id="ftpsPath" placeholder="/process/OK/">
</div>
<div class="field">
<label>傳輸模式</label>
<select id="ftpsMode">
<option value="explicit">明文 FTPS(Explicit)</option>
<option value="implicit">隱式 FTPS(Implicit)</option>
</select>
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-outline" style="flex:1;" id="testFtpsBtn">🔌 測試連線</button>
<button class="btn btn-gray" style="flex:1;" id="cancelFtpsBtn">取消</button>
<button class="btn btn-green" style="flex:1;" id="saveFtpsBtn">儲存</button>
</div>
</div>
</div>

<!-- Save action modal -->
<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 class="check-circle">✓</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>
// ---- 類別(檔名)清單 ----
let categories = [
{ key:"OK", file:"OK.xml" },
{ key:"OK_myok", file:"OK_myok.xml" },
{ key:"RGP", file:"RGP.xml" },
{ key:"scl_FPs", file:"scl_FPs.xml" },
{ key:"scl_iDealSoft", file:"scl_iDealSoft.xml" }
];

// ---- 由現有 Excel 匯入之種子資料 ----
const seedData = {"OK": [{"order": 1, "parCode": "9", "zone": "", "dtype": "1:處方規格內容", "spec": "FK", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 2, "parCode": "16", "zone": "", "dtype": "1:處方規格內容", "spec": "SK", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 3, "parCode": "26", "zone": "", "dtype": "1:處方規格內容", "spec": "AXIS", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 4, "parCode": "0", "zone": "", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 5, "parCode": "1", "zone": "", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 6, "parCode": "4", "zone": "", "dtype": "1:處方規格內容", "spec": "OZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 7, "parCode": "32", "zone": "", "dtype": "1:處方規格內容", "spec": "CT", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 8, "parCode": "56", "zone": "", "dtype": "1:處方規格內容", "spec": "Target", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 9, "parCode": "7", "zone": "", "dtype": "3:固定值", "spec": "", "fixed": "0.45", "op1": "", "opMode": "", "op2": ""}, {"order": 10, "parCode": "63", "zone": "", "dtype": "1:處方規格內容", "spec": "FC Type", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 11, "parCode": "64", "zone": "", "dtype": "1:處方規格內容", "spec": "AC Type", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 12, "parCode": "65", "zone": "", "dtype": "1:處方規格內容", "spec": "AC2 Type", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 13, "parCode": "2", "zone": "", "dtype": "1:處方規格內容", "spec": "Edge Th", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 14, "parCode": "3", "zone": "", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 15, "parCode": "12", "zone": "", "dtype": "1:處方規格內容", "spec": "BCE", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 16, "parCode": "19", "zone": "", "dtype": "1:處方規格內容", "spec": "CYL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 17, "parCode": "28", "zone": "", "dtype": "1:處方規格內容", "spec": "Edge Lift", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 18, "parCode": "45", "zone": "", "dtype": "1:處方規格內容", "spec": "AEL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 19, "parCode": "46", "zone": "", "dtype": "1:處方規格內容", "spec": "PRP", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 20, "parCode": "66", "zone": "", "dtype": "1:處方規格內容", "spec": "RZD", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 21, "parCode": "84", "zone": "", "dtype": "1:處方規格內容", "spec": "PBO", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 22, "parCode": "118", "zone": "5", "dtype": "4:處方規格運算", "spec": "", "fixed": "", "op1": "PC", "opMode": "1:加", "op2": "PCAdd"}, {"order": 23, "parCode": "10", "zone": "", "dtype": "4:處方規格運算", "spec": "", "fixed": "", "op1": "CRI", "opMode": "4:除", "op2": "FK"}, {"order": 24, "parCode": "14", "zone": "", "dtype": "4:處方規格運算", "spec": "", "fixed": "", "op1": "CRI", "opMode": "4:除", "op2": "SK"}, {"order": 25, "parCode": "13", "zone": "", "dtype": "1:處方規格內容", "spec": "Steep CE", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 26, "parCode": "67", "zone": "", "dtype": "1:處方規格內容", "spec": "AZE", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 27, "parCode": "68", "zone": "", "dtype": "1:處方規格內容", "spec": "Steep ACR", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 28, "parCode": "93", "zone": "", "dtype": "1:處方規格內容", "spec": "Steep AZE", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 29, "parCode": "405", "zone": "", "dtype": "1:處方規格內容", "spec": "Peripheral Defocus", "fixed": "", "op1": "", "opMode": "", "op2": ""}], "OK_myok": [{"order": 1, "parCode": "9", "zone": "", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 2, "parCode": "0", "zone": "", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 3, "parCode": "4", "zone": "", "dtype": "1:處方規格內容", "spec": "OZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 4, "parCode": "1", "zone": "", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 5, "parCode": "118", "zone": "2", "dtype": "1:處方規格內容", "spec": "MK-RC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 6, "parCode": "402", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Flat Ro", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 7, "parCode": "403", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Flat E", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 8, "parCode": "404", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-AC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 9, "parCode": "405", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-AC E", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 10, "parCode": "248", "zone": "2", "dtype": "1:處方規格內容", "spec": "MK-Steep RC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 11, "parCode": "406", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Steep Ro", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 12, "parCode": "407", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Steep E", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 13, "parCode": "408", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Steep AC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 14, "parCode": "409", "zone": "", "dtype": "1:處方規格內容", "spec": "MK-Steep AC E", "fixed": "", "op1": "", "opMode": "", "op2": ""}], "RGP": [{"order": 1, "parCode": "9", "zone": "", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 2, "parCode": "16", "zone": "", "dtype": "1:處方規格內容", "spec": "BC2", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 3, "parCode": "60", "zone": "", "dtype": "1:處方規格內容", "spec": "AXIS", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 4, "parCode": "0", "zone": "", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 5, "parCode": "19", "zone": "", "dtype": "1:處方規格內容", "spec": "CYL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 6, "parCode": "59", "zone": "", "dtype": "1:處方規格內容", "spec": "AF", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 7, "parCode": "1", "zone": "", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 8, "parCode": "2", "zone": "", "dtype": "1:處方規格內容", "spec": "Edge Th", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 9, "parCode": "3", "zone": "", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 10, "parCode": "4", "zone": "", "dtype": "1:處方規格內容", "spec": "OZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 11, "parCode": "12", "zone": "", "dtype": "1:處方規格內容", "spec": "BCE", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 12, "parCode": "28", "zone": "", "dtype": "1:處方規格內容", "spec": "Edge Lift", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 13, "parCode": "32", "zone": "", "dtype": "1:處方規格內容", "spec": "CT", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 14, "parCode": "45", "zone": "", "dtype": "1:處方規格內容", "spec": "AEL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 15, "parCode": "46", "zone": "", "dtype": "1:處方規格內容", "spec": "PRP", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 16, "parCode": "58", "zone": "", "dtype": "1:處方規格內容", "spec": "ADD", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 17, "parCode": "66", "zone": "", "dtype": "1:處方規格內容", "spec": "RZD", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 18, "parCode": "84", "zone": "", "dtype": "1:處方規格內容", "spec": "Angle Offset", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 19, "parCode": "141", "zone": "2", "dtype": "1:處方規格內容", "spec": "2nd ZR", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 20, "parCode": "142", "zone": "3", "dtype": "1:處方規格內容", "spec": "3rd ZR", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 21, "parCode": "143", "zone": "4", "dtype": "1:處方規格內容", "spec": "4th ZR", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 22, "parCode": "144", "zone": "2", "dtype": "1:處方規格內容", "spec": "2nd ZW", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 23, "parCode": "145", "zone": "3", "dtype": "1:處方規格內容", "spec": "3rd ZW", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 24, "parCode": "63", "zone": "", "dtype": "1:處方規格內容", "spec": "Aux1", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 25, "parCode": "64", "zone": "", "dtype": "1:處方規格內容", "spec": "Aux2", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 26, "parCode": "65", "zone": "", "dtype": "1:處方規格內容", "spec": "Aux3", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 27, "parCode": "49", "zone": "", "dtype": "1:處方規格內容", "spec": "RK-K2 lift", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 28, "parCode": "409", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-SAG", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 29, "parCode": "404", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-Dual Depth Axis", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 30, "parCode": "400", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-PCZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 31, "parCode": "401", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-LLZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 32, "parCode": "402", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-SLZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 33, "parCode": "403", "zone": "", "dtype": "1:處方規格內容", "spec": "AP-LLZ steep", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 34, "parCode": "63", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 2 @ 0", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 35, "parCode": "64", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 2 @ 090", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 36, "parCode": "65", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 2 @ 180", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 37, "parCode": "66", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 2 @ 270", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 38, "parCode": "115", "zone": "2", "dtype": "1:處方規格內容", "spec": "Zone 2 Width", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 39, "parCode": "67", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 3 @ 0", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 40, "parCode": "68", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 3 @ 090", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 41, "parCode": "93", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 3 @ 180", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 42, "parCode": "94", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 3 @ 270", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 43, "parCode": "115", "zone": "3", "dtype": "1:處方規格內容", "spec": "Zone 3 Width", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 44, "parCode": "95", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 4 @ 0", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 45, "parCode": "96", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 4 @ 090", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 46, "parCode": "400", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 4 @ 180", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 47, "parCode": "401", "zone": "", "dtype": "1:處方規格內容", "spec": "Zone 4 @ 270", "fixed": "", "op1": "", "opMode": "", "op2": ""}], "scl_FPs": [{"order": 1, "parCode": "9", "zone": "", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 2, "parCode": "1", "zone": "", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 3, "parCode": "0", "zone": "", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 4, "parCode": "20", "zone": "", "dtype": "1:處方規格內容", "spec": "CYL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 5, "parCode": "21", "zone": "", "dtype": "1:處方規格內容", "spec": "AXIS", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 6, "parCode": "23", "zone": "", "dtype": "1:處方規格內容", "spec": "ADD", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 7, "parCode": "22", "zone": "", "dtype": "1:處方規格內容", "spec": "DOMI", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 8, "parCode": "30", "zone": "", "dtype": "1:處方規格內容", "spec": "PC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 9, "parCode": "31", "zone": "", "dtype": "1:處方規格內容", "spec": "A1", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 10, "parCode": "32", "zone": "", "dtype": "1:處方規格內容", "spec": "A2", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 11, "parCode": "33", "zone": "", "dtype": "1:處方規格內容", "spec": "A3", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 12, "parCode": "34", "zone": "", "dtype": "1:處方規格內容", "spec": "A4", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 13, "parCode": "35", "zone": "", "dtype": "1:處方規格內容", "spec": "SMC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 14, "parCode": "43", "zone": "", "dtype": "1:處方規格內容", "spec": "N Zone", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 15, "parCode": "44", "zone": "", "dtype": "1:處方規格內容", "spec": "D Zone", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 16, "parCode": "8", "zone": "", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}], "scl_iDealSoft": [{"order": 1, "parCode": "9", "zone": "", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 2, "parCode": "1", "zone": "", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 3, "parCode": "0", "zone": "", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 4, "parCode": "20", "zone": "", "dtype": "1:處方規格內容", "spec": "CYL", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 5, "parCode": "21", "zone": "", "dtype": "1:處方規格內容", "spec": "AXIS", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 6, "parCode": "23", "zone": "", "dtype": "1:處方規格內容", "spec": "ADD", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 7, "parCode": "22", "zone": "", "dtype": "1:處方規格內容", "spec": "DOMI", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 8, "parCode": "43", "zone": "", "dtype": "1:處方規格內容", "spec": "N Zone", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 9, "parCode": "44", "zone": "", "dtype": "1:處方規格內容", "spec": "D Zone", "fixed": "", "op1": "", "opMode": "", "op2": ""}, {"order": 10, "parCode": "8", "zone": "", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": ""}]};

// deep clone as working state
let data = JSON.parse(JSON.stringify(seedData));
let activeCat = "OK";
let savedState = {}; // key -> {time, path}

// ---- 編輯鎖定狀態:預設檢視(唯讀),點「修改」才能編輯,儲存或取消後鎖回 ----
let editMode = false;
let editSnapshot = null; // 進入編輯模式時,該類別列資料的快照,供「取消」還原

function updateEditModeUI(){
document.getElementById("editModeBtn").style.display = editMode ? "none" : "inline-flex";
document.getElementById("cancelEditBtn").style.display = editMode ? "inline-flex" : "none";
document.getElementById("addRowBtn").style.display = editMode ? "inline-flex" : "none";
document.getElementById("deleteCatBtn").style.display = editMode ? "inline-flex" : "none";
document.getElementById("lockedHint").style.display = editMode ? "none" : "flex";
document.getElementById("matchRuleRow").style.display = editMode ? "flex" : "none";
const dupSpecs = findDuplicates(data[activeCat]||[]);
const saveBtn = document.getElementById("saveXmlBtn");
saveBtn.style.display = editMode ? "inline-flex" : "none";
saveBtn.disabled = dupSpecs.size>0;
}

// ---- 每個類別(檔案)各自的 FTPS 上傳設定 ----
let ftpsSettings = {
OK: { host:"ftps.paramsvr.local", port:"21", user:"ftp_ok", pass:"OkPass!234", path:"/process/OK/", mode:"explicit" },
OK_myok: { host:"ftps.paramsvr.local", port:"21", user:"ftp_okmyok", pass:"MyokPass!234", path:"/process/OK_myok/", mode:"explicit" },
RGP: { host:"ftps.paramsvr.local", port:"21", user:"ftp_rgp", pass:"RgpPass!234", path:"/process/RGP/", mode:"explicit" },
scl_FPs: { host:"ftps.paramsvr.local", port:"21", user:"ftp_sclfps", pass:"", path:"", mode:"explicit" },
scl_iDealSoft: { host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" }
};

function escapeHtml(s){
return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
}

// ---- 類別自動比對規則(Regex):依 客戶/產品設計碼/產品類別 決定是否套用此加工參數範本 ----
let matchRules = {
OK: [
{ customer:"^(HOYA|B&L).*", designCode:"^FK.*", prodCategory:"一般片", desc:"HOYA/B&L 一般片,FK 系列設計碼", enabled:true },
{ customer:"", designCode:"", prodCategory:"^myOK.*", desc:"myOK 系列產品類別,客戶/設計碼不拘(兜底規則)", enabled:true }
],
OK_myok: [],
RGP: [],
scl_FPs: [],
scl_iDealSoft: []
};

function isValidRegex(pattern){
try{ new RegExp(pattern); return true; } catch(e){ return false; }
}
function fieldMatches(pattern, value){
if(!pattern || pattern.trim()==="") return true;
if(!isValidRegex(pattern)) return false;
return new RegExp(pattern).test(value||"");
}
function runMatchRuleTest(rules, customer, designCode, prodCategory){
for(const r of rules){
if(!r.enabled) continue;
if(fieldMatches(r.customer, customer) && fieldMatches(r.designCode, designCode) && fieldMatches(r.prodCategory, prodCategory)){
return r;
}
}
return null;
}
function updateMatchRuleSummary(){
document.getElementById("matchRuleCount").textContent = (matchRules[activeCat]||[]).length;
}

// ---- 比對規則編輯 modal ----
let editingMatchRules = [];
const matchRuleModal = document.getElementById("matchRuleModal");

function openMatchRuleModal(){
if(!editMode) return;
editingMatchRules = JSON.parse(JSON.stringify(matchRules[activeCat]||[]));
const cat = categories.find(c=>c.key===activeCat);
document.getElementById("matchRuleModalTitle").textContent = `類別自動比對規則 — ${activeCat}`;
document.getElementById("matchRuleModalSub").textContent = `符合條件時,系統將自動套用「${cat.file}」作為此工單的加工參數範本`;
document.getElementById("mrTestCustomer").value = "";
document.getElementById("mrTestDesignCode").value = "";
document.getElementById("mrTestProdCategory").value = "";
document.getElementById("mrTestResult").innerHTML = "";
renderMatchRuleTable();
matchRuleModal.classList.add("show");
}
document.getElementById("editMatchRulesBtn").addEventListener("click", openMatchRuleModal);

function renderMatchRuleTable(){
const tbody = document.getElementById("matchRuleTableBody");
tbody.innerHTML = "";
editingMatchRules.forEach((r,i)=>{
const validCustomer = isValidRegex(r.customer);
const validDesign = isValidRegex(r.designCode);
const validProd = isValidRegex(r.prodCategory);
const tr = document.createElement("tr");
tr.innerHTML = `
<td class="rule-order">${i+1}</td>
<td>
<input type="text" class="rule-input wide ${validCustomer?"":"invalid"}" data-i="${i}" data-role="customer" value="${escapeHtml(r.customer)}" placeholder="例如 ^HOYA.*(留空=不限)">
${validCustomer?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
</td>
<td>
<input type="text" class="rule-input wide ${validDesign?"":"invalid"}" data-i="${i}" data-role="designCode" value="${escapeHtml(r.designCode)}" placeholder="例如 ^FK.*(留空=不限)">
${validDesign?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
</td>
<td>
<input type="text" class="rule-input wide ${validProd?"":"invalid"}" data-i="${i}" data-role="prodCategory" value="${escapeHtml(r.prodCategory)}" placeholder="例如 一般片(留空=不限)">
${validProd?"":'<div class="regex-error">⚠ 無效的正規表示式</div>'}
</td>
<td><input type="text" class="rule-input" data-i="${i}" data-role="desc" value="${escapeHtml(r.desc)}" placeholder="說明(選填)"></td>
<td style="text-align:center;">
<label class="toggle">
<input type="checkbox" data-i="${i}" data-role="enabled" ${r.enabled?"checked":""}>
<span class="slider"></span>
</label>
</td>
<td>
<div class="row-actions">
<button class="icon-btn plain" data-i="${i}" data-action="move-up" ${i===0?"disabled":""} title="上移">↑</button>
<button class="icon-btn plain" data-i="${i}" data-action="move-down" ${i===editingMatchRules.length-1?"disabled":""} title="下移">↓</button>
</div>
</td>
<td><button class="icon-btn" data-i="${i}" data-action="delete-rule" title="刪除">🗑</button></td>
`;
tbody.appendChild(tr);
});
document.getElementById("matchRuleCountLabel").textContent = `共 ${editingMatchRules.length} 條規則`;
}

document.getElementById("matchRuleTableBody").addEventListener("input", e=>{
const role = e.target.dataset.role;
const i = e.target.dataset.i;
if(i===undefined || !role) return;
editingMatchRules[i][role] = e.target.value;
if(role==="customer" || role==="designCode" || role==="prodCategory"){
const valid = isValidRegex(e.target.value);
e.target.classList.toggle("invalid", !valid);
const errDiv = e.target.parentElement.querySelector(".regex-error");
if(!valid && !errDiv){
const div = document.createElement("div");
div.className = "regex-error";
div.textContent = "⚠ 無效的正規表示式";
e.target.parentElement.appendChild(div);
} else if(valid && errDiv){
errDiv.remove();
}
}
});

document.getElementById("matchRuleTableBody").addEventListener("change", e=>{
const role = e.target.dataset.role;
const i = e.target.dataset.i;
if(role==="enabled") editingMatchRules[i].enabled = e.target.checked;
});

document.getElementById("matchRuleTableBody").addEventListener("click", e=>{
const btn = e.target.closest("button[data-action]");
if(!btn) return;
const i = Number(btn.dataset.i);
const action = btn.dataset.action;
if(action==="delete-rule"){ editingMatchRules.splice(i,1); renderMatchRuleTable(); }
if(action==="move-up" && i>0){ [editingMatchRules[i-1],editingMatchRules[i]]=[editingMatchRules[i],editingMatchRules[i-1]]; renderMatchRuleTable(); }
if(action==="move-down" && i<editingMatchRules.length-1){ [editingMatchRules[i+1],editingMatchRules[i]]=[editingMatchRules[i],editingMatchRules[i+1]]; renderMatchRuleTable(); }
});

document.getElementById("addMatchRuleBtn").addEventListener("click", ()=>{
editingMatchRules.push({ customer:"", designCode:"", prodCategory:"", desc:"", enabled:true });
renderMatchRuleTable();
const inputs = document.querySelectorAll('#matchRuleTableBody input[data-role="customer"]');
const last = inputs[inputs.length-1];
if(last) last.focus();
});

document.getElementById("closeMatchRuleModalBtn").addEventListener("click", ()=> matchRuleModal.classList.remove("show"));
document.getElementById("cancelMatchRuleBtn").addEventListener("click", ()=> matchRuleModal.classList.remove("show"));

document.getElementById("applyMatchRuleBtn").addEventListener("click", ()=>{
const hasInvalid = editingMatchRules.some(r=>
(r.customer.trim()!=="" && !isValidRegex(r.customer)) ||
(r.designCode.trim()!=="" && !isValidRegex(r.designCode)) ||
(r.prodCategory.trim()!=="" && !isValidRegex(r.prodCategory))
);
if(hasInvalid){
showToast("有規則的正規表示式格式錯誤,請修正後再套用");
return;
}
matchRules[activeCat] = JSON.parse(JSON.stringify(editingMatchRules));
matchRuleModal.classList.remove("show");
updateMatchRuleSummary();
showToast(`已更新「${activeCat}」的比對規則,別忘了點「儲存」`);
});

document.getElementById("runMatchTestBtn").addEventListener("click", ()=>{
const customer = document.getElementById("mrTestCustomer").value;
const designCode = document.getElementById("mrTestDesignCode").value;
const prodCategory = document.getElementById("mrTestProdCategory").value;
const resultBox = document.getElementById("mrTestResult");
const result = runMatchRuleTest(editingMatchRules, customer, designCode, prodCategory);
if(result){
const idx = editingMatchRules.indexOf(result);
resultBox.innerHTML = `<div class="test-result hit">
✓ 命中規則 #${idx+1}:客戶 <code>/${escapeHtml(result.customer||".*")}/</code>、產品設計碼 <code>/${escapeHtml(result.designCode||".*")}/</code>、產品類別 <code>/${escapeHtml(result.prodCategory||".*")}/</code><br>
將自動套用本類別(${activeCat})作為加工參數範本${result.desc?`(${escapeHtml(result.desc)})`:""}
</div>`;
} else {
resultBox.innerHTML = `<div class="test-result miss">✕ 沒有規則命中,請檢查條件或新增兜底規則(三個欄位皆留空即符合所有工單)</div>`;
}
});

function ftpsIsSet(key){
const f = ftpsSettings[key];
return !!(f && f.host && f.user && f.path);
}
function ftpsDestString(key){
const f = ftpsSettings[key];
if(!ftpsIsSet(key)) return null;
return `${f.user}@${f.host}:${f.port||21}${f.path}`;
}

const dtypeOptions = ["1:處方規格內容","3:固定值","4:處方規格運算"];
const opModeOptions = ["1:加","2:減","3:乘","4:除"];

// unique spec list for datalist suggestions
const allSpecs = [...new Set(Object.values(seedData).flat().map(r=>r.spec).filter(Boolean))].sort();
document.getElementById("specCodes").innerHTML = allSpecs.map(s=>`<option value="${s}">`).join("");

function typeClass(dtype){
if(dtype.startsWith("1:")) return "type-1";
if(dtype.startsWith("3:")) return "type-3";
return "type-4";
}

function renderTabs(){
const wrap = document.getElementById("catTabs");
wrap.innerHTML = categories.map(c=>{
const cnt = data[c.key] ? data[c.key].length : 0;
return `<div class="cat-tab ${c.key===activeCat?'active':''}" data-cat="${c.key}">
${c.key} <span class="fname">${c.file}</span><span class="count">${cnt}</span>
</div>`;
}).join("") + `<div class="cat-tab add-tab" id="addCatTabBtn">+ 新增類別</div>`;
wrap.querySelectorAll(".cat-tab[data-cat]").forEach(el=>{
el.addEventListener("click", ()=>{
if(editMode && el.dataset.cat!==activeCat){
showToast("請先儲存或取消目前的編輯,才能切換類別");
return;
}
activeCat = el.dataset.cat;
renderTabs();
renderTable();
});
});
document.getElementById("addCatTabBtn").addEventListener("click", openAddCatModal);
}

// ---- 新增空白類別 ----
const addCatModal = document.getElementById("addCatModal");
function openAddCatModal(){
if(editMode){
showToast("請先儲存或取消目前的編輯,才能新增類別");
return;
}
document.getElementById("newCatKey").value = "";
document.getElementById("newCatFile").value = "";
document.getElementById("newCatFile").dataset.touched = "";
addCatModal.classList.add("show");
setTimeout(()=> document.getElementById("newCatKey").focus(), 50);
}
document.getElementById("cancelAddCatBtn").addEventListener("click", ()=> addCatModal.classList.remove("show"));
document.getElementById("newCatKey").addEventListener("input", (e)=>{
const fileInput = document.getElementById("newCatFile");
if(!fileInput.dataset.touched){
fileInput.value = e.target.value.trim() ? `${e.target.value.trim()}.xml` : "";
}
});
document.getElementById("newCatFile").addEventListener("input", (e)=>{ e.target.dataset.touched = "1"; });

document.getElementById("confirmAddCatBtn").addEventListener("click", ()=>{
const key = document.getElementById("newCatKey").value.trim();
let file = document.getElementById("newCatFile").value.trim();
if(!key){ showToast("請輸入類別代碼"); return; }
if(categories.some(c=>c.key===key)){ showToast(`類別代碼「${key}」已存在`); return; }
if(!file){ file = `${key}.xml`; }
if(!file.toLowerCase().endsWith(".xml")){ file += ".xml"; }

categories.push({ key, file });
data[key] = [];
ftpsSettings[key] = { host:"", port:"21", user:"", pass:"", path:"", mode:"explicit" };
matchRules[key] = [];

activeCat = key;
editMode = true;
editSnapshot = { rows: [], matchRules: [] };
addCatModal.classList.remove("show");
renderTabs();
renderTable();
showToast(`已建立空白類別「${key}」,目前已進入編輯模式,可直接新增列並儲存`);
});

// ---- 通用確認刪除 modal ----
const confirmModal = document.getElementById("confirmModal");
let confirmCallback = null;
function openConfirm(title, desc, onConfirm){
document.getElementById("confirmTitle").textContent = title;
document.getElementById("confirmDesc").textContent = desc;
confirmCallback = onConfirm;
confirmModal.classList.add("show");
}
document.getElementById("confirmCancelBtn").addEventListener("click", ()=>{
confirmModal.classList.remove("show");
confirmCallback = null;
});
document.getElementById("confirmOkBtn").addEventListener("click", ()=>{
confirmModal.classList.remove("show");
if(confirmCallback) confirmCallback();
confirmCallback = null;
});

// ---- 刪除整個類別(例如新增類別後發現有問題,可整份刪除)----
document.getElementById("deleteCatBtn").addEventListener("click", ()=>{
if(!editMode) return;
if(categories.length<=1){
showToast("至少需保留一個類別,無法刪除");
return;
}
const cat = categories.find(c=>c.key===activeCat);
openConfirm(
`刪除類別「${activeCat}」?`,
`即將刪除 ${cat.file},包含其所有列資料與 FTPS 設定,此動作無法復原。`,
()=>{
const deletedKey = activeCat;
categories = categories.filter(c=>c.key!==deletedKey);
delete data[deletedKey];
delete ftpsSettings[deletedKey];
delete savedState[deletedKey];
delete matchRules[deletedKey];
editMode = false;
editSnapshot = null;
activeCat = categories[0].key;
renderTabs();
renderTable();
showToast(`已刪除類別「${deletedKey}」`);
}
);
});

function findDuplicates(rows){
const seen = {};
const dupSpecs = new Set();
rows.forEach(r=>{
if(r.dtype.startsWith("1:") && r.spec.trim()!==""){
const key = r.spec.trim().toLowerCase();
seen[key] = (seen[key]||0)+1;
if(seen[key]>1) dupSpecs.add(r.spec.trim());
}
});
return dupSpecs;
}

function renderTable(){
const cat = categories.find(c=>c.key===activeCat);
document.getElementById("catTitle").textContent = `${activeCat} 加工參數列表`;
document.getElementById("fileNameLabel").textContent = cat.file;
const dest = ftpsDestString(activeCat);
document.getElementById("serverPathLabel").textContent = dest ? `${dest}${cat.file}` : "尚未設定 FTPS";
const destLabel = document.getElementById("ftpsDestLabel");
destLabel.textContent = dest || "未設定";
destLabel.classList.toggle("unset", !dest);
const saved = savedState[activeCat];
document.getElementById("lastSaved").textContent = saved ? `上次儲存:${saved.time}` : "尚未儲存";

const rows = data[activeCat];
const dupSpecs = findDuplicates(rows);

const body = document.getElementById("rowsBody");
body.innerHTML = "";
rows.forEach((r, idx)=>{
const isDup = r.dtype.startsWith("1:") && r.spec.trim()!=="" && dupSpecs.has(r.spec.trim());
const tr = document.createElement("tr");
tr.dataset.idx = idx;
if(isDup) tr.classList.add("dup");

const isType1 = r.dtype.startsWith("1:");
const isType3 = r.dtype.startsWith("3:");
const isType4 = r.dtype.startsWith("4:");

const lockAttr = editMode ? "" : "disabled";
tr.innerHTML = `
<td class="order-num">${idx+1}</td>
<td><input type="text" class="cell-input small" data-role="parCode" value="${r.parCode}" ${lockAttr}></td>
<td><input type="text" class="cell-input small" data-role="zone" value="${r.zone}" ${lockAttr}></td>
<td>
<select class="cell-select" data-role="dtype" ${lockAttr}>
${dtypeOptions.map(o=>`<option value="${o}" ${o===r.dtype?"selected":""}>${o}</option>`).join("")}
</select>
<span class="type-badge ${typeClass(r.dtype)}">${r.dtype.split(":")[1]}</span>
</td>
<td><input type="text" list="specCodes" class="cell-input wide ${isDup?'error':''}" data-role="spec" value="${r.spec}" ${(editMode && isType1)?"":"disabled"} placeholder="${isType1?'例如 BC / DIA / Power':''}"></td>
<td><input type="text" class="cell-input small" data-role="fixed" value="${r.fixed}" ${(editMode && isType3)?"":"disabled"}></td>
<td><input type="text" list="specCodes" class="cell-input wide" data-role="op1" value="${r.op1}" ${(editMode && isType4)?"":"disabled"}></td>
<td>
<select class="cell-select" data-role="opMode" ${(editMode && isType4)?"":"disabled"}>
<option value="">-</option>
${opModeOptions.map(o=>`<option value="${o}" ${o===r.opMode?"selected":""}>${o}</option>`).join("")}
</select>
</td>
<td><input type="text" list="specCodes" class="cell-input wide" data-role="op2" value="${r.op2}" ${(editMode && isType4)?"":"disabled"}></td>
<td><button class="icon-btn" data-action="delete" title="刪除此列" ${lockAttr}>🗑</button></td>
`;
body.appendChild(tr);
});

document.getElementById("rowCount").textContent = rows.length;

// duplicate alert
const alertBox = document.getElementById("dupAlert");
if(dupSpecs.size>0){
alertBox.style.display = "block";
alertBox.innerHTML = `<div class="alert alert-error"><span class="ico">⚠️</span><div>發現 ${dupSpecs.size} 個重複的處方規格代碼:<b>${[...dupSpecs].join("、")}</b>。處方規格代碼為主鍵,請修正後再儲存,目前無法轉換為 XML。</div></div>`;
} else {
alertBox.style.display = "none";
alertBox.innerHTML = "";
}
updateMatchRuleSummary();
updateEditModeUI();
}

// Text-field edits: update data model + lightweight validation refresh only
// (no full re-render, so the input keeps focus/cursor while typing)
document.getElementById("rowsBody").addEventListener("input", e=>{
if(!editMode) return;
const role = e.target.dataset.role;
if(!role) return;
const tr = e.target.closest("tr");
const idx = Number(tr.dataset.idx);
data[activeCat][idx][role] = e.target.value;
if(role==="spec") refreshValidationOnly();
});

document.getElementById("rowsBody").addEventListener("change", e=>{
if(!editMode) return;
const role = e.target.dataset.role;
if(role!=="dtype") return;
const tr = e.target.closest("tr");
const idx = Number(tr.dataset.idx);
data[activeCat][idx].dtype = e.target.value;
renderTabs();
renderTable();
});

document.getElementById("rowsBody").addEventListener("click", e=>{
if(!editMode) return;
const btn = e.target.closest("button[data-action]");
if(!btn) return;
const tr = btn.closest("tr");
const idx = Number(tr.dataset.idx);
data[activeCat].splice(idx,1);
data[activeCat].forEach((r,i)=> r.order = i+1);
renderTabs();
renderTable();
showToast("已刪除該列");
});

// Lightweight refresh: only toggles duplicate highlighting / alert / save-button state,
// without rebuilding the row inputs (keeps focus while typing)
function refreshValidationOnly(){
const rows = data[activeCat];
const dupSpecs = findDuplicates(rows);
const body = document.getElementById("rowsBody");
[...body.children].forEach(tr=>{
const idx = Number(tr.dataset.idx);
const r = rows[idx];
const isDup = r.dtype.startsWith("1:") && r.spec.trim()!=="" && dupSpecs.has(r.spec.trim());
tr.classList.toggle("dup", isDup);
const specInput = tr.querySelector('input[data-role="spec"]');
if(specInput) specInput.classList.toggle("error", isDup);
});
const alertBox = document.getElementById("dupAlert");
if(dupSpecs.size>0){
alertBox.style.display = "block";
alertBox.innerHTML = `<div class="alert alert-error"><span class="ico">⚠️</span><div>發現 ${dupSpecs.size} 個重複的處方規格代碼:<b>${[...dupSpecs].join("、")}</b>。處方規格代碼為主鍵,請修正後再儲存,目前無法轉換為 XML。</div></div>`;
} else {
alertBox.style.display = "none";
alertBox.innerHTML = "";
}
document.getElementById("saveXmlBtn").disabled = !editMode || dupSpecs.size>0;
}

document.getElementById("editModeBtn").addEventListener("click", ()=>{
editMode = true;
editSnapshot = {
rows: JSON.parse(JSON.stringify(data[activeCat])),
matchRules: JSON.parse(JSON.stringify(matchRules[activeCat]||[]))
};
renderTable();
});

document.getElementById("cancelEditBtn").addEventListener("click", ()=>{
if(editSnapshot){
data[activeCat] = editSnapshot.rows;
matchRules[activeCat] = editSnapshot.matchRules;
}
editMode = false;
editSnapshot = null;
renderTabs();
renderTable();
showToast("已取消編輯,變更未儲存");
});

document.getElementById("addRowBtn").addEventListener("click", ()=>{
if(!editMode) return;
const rows = data[activeCat];
rows.push({
order: rows.length+1,
parCode:"",
zone:"",
dtype:"1:處方規格內容",
spec:"",
fixed:"",
op1:"",
opMode:"",
op2:""
});
renderTabs();
renderTable();
const body = document.getElementById("rowsBody");
const lastRow = body.lastElementChild;
if(lastRow){
const specInput = lastRow.querySelector('input[data-role="spec"]');
if(specInput) specInput.focus();
}
});

// XML preview
function buildXml(catKey){
const cat = categories.find(c=>c.key===catKey);
const rows = data[catKey];
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<ProcessTemplate category="${catKey}" file="${cat.file}">\n`;
rows.forEach(r=>{
xml += ` <Parameter order="${r.order}" parCode="${r.parCode}"${r.zone?` zone="${r.zone}"`:""} type="${r.dtype}">\n`;
if(r.dtype.startsWith("1:")){
xml += ` <SpecCode>${escapeXml(r.spec)}</SpecCode>\n`;
} else if(r.dtype.startsWith("3:")){
xml += ` <FixedValue>${escapeXml(r.fixed)}</FixedValue>\n`;
} else if(r.dtype.startsWith("4:")){
xml += ` <Calc op1="${escapeXml(r.op1)}" mode="${escapeXml(r.opMode)}" op2="${escapeXml(r.op2)}"/>\n`;
}
xml += ` </Parameter>\n`;
});
xml += `</ProcessTemplate>`;
return xml;
}
function escapeXml(s){ return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }

const xmlModal = document.getElementById("xmlModal");
document.getElementById("previewXmlBtn").addEventListener("click", ()=>{
const cat = categories.find(c=>c.key===activeCat);
document.getElementById("xmlModalTitle").textContent = `XML 預覽 — ${cat.file}`;
document.getElementById("xmlModalSub").textContent = `共 ${data[activeCat].length} 個 Parameter 節點(僅預覽,尚未寫入 Server)`;
document.getElementById("xmlPreviewContent").textContent = buildXml(activeCat);
xmlModal.classList.add("show");
});
document.getElementById("closeXmlBtn").addEventListener("click", ()=> xmlModal.classList.remove("show"));

// ---- FTPS settings modal ----
const ftpsModal = document.getElementById("ftpsModal");

function openFtpsModal(){
const cat = categories.find(c=>c.key===activeCat);
const f = ftpsSettings[activeCat];
document.getElementById("ftpsModalTitle").textContent = `FTPS 連線設定 — ${activeCat}`;
document.getElementById("ftpsModalSub").textContent = `此設定僅套用於 ${cat.file},每個類別各自獨立`;
document.getElementById("ftpsHost").value = f.host;
document.getElementById("ftpsPort").value = f.port;
document.getElementById("ftpsUser").value = f.user;
document.getElementById("ftpsPass").value = f.pass;
document.getElementById("ftpsPath").value = f.path;
document.getElementById("ftpsMode").value = f.mode;
document.getElementById("ftpsPass").type = "password";
document.getElementById("toggleFtpsPw").textContent = "顯示";
ftpsModal.classList.add("show");
}
document.getElementById("ftpsBtn").addEventListener("click", openFtpsModal);
document.getElementById("cancelFtpsBtn").addEventListener("click", ()=> ftpsModal.classList.remove("show"));
document.getElementById("toggleFtpsPw").addEventListener("click", (e)=>{
const input = document.getElementById("ftpsPass");
if(input.type==="password"){ input.type="text"; e.target.textContent="隱藏"; }
else { input.type="password"; e.target.textContent="顯示"; }
});

document.getElementById("saveFtpsBtn").addEventListener("click", ()=>{
ftpsSettings[activeCat] = {
host: document.getElementById("ftpsHost").value.trim(),
port: document.getElementById("ftpsPort").value.trim() || "21",
user: document.getElementById("ftpsUser").value.trim(),
pass: document.getElementById("ftpsPass").value,
path: document.getElementById("ftpsPath").value.trim(),
mode: document.getElementById("ftpsMode").value
};
ftpsModal.classList.remove("show");
renderTable();
showToast(`已更新「${activeCat}」的 FTPS 設定`);
});

document.getElementById("testFtpsBtn").addEventListener("click", ()=>{
const host = document.getElementById("ftpsHost").value.trim();
const user = document.getElementById("ftpsUser").value.trim();
const port = document.getElementById("ftpsPort").value.trim() || "21";
const path = document.getElementById("ftpsPath").value.trim();
const mode = document.getElementById("ftpsMode").value;
if(!host || !user){
showToast("請至少填寫 Host 與帳號再測試連線");
return;
}
runModalSequence({
title:"測試 FTPS 連線",
desc:`正在連線至 ${host}:${port}`,
steps:[
`建立 ${mode==="implicit"?"隱式":"明文"} FTPS 連線...`,
`以帳號「${user}」登入...`,
`切換至路徑 ${path || "/"}...`,
"驗證讀寫權限..."
],
onDone: ()=> showModalResult({
title:"連線成功",
desc:`已成功連線並登入 ${host}`,
log:[`✓ TLS 交握成功`, `✓ 帳號驗證通過`, `✓ 路徑 ${path || "/"} 可讀寫`]
})
});
});

// Save (convert + upload) action
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);
}
}, 260);
}
function showModalResult({title, desc, log}){
modalLoading.style.display="none";
modalResult.style.display="block";
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"));

document.getElementById("saveXmlBtn").addEventListener("click", ()=>{
if(!editMode) return;
const dupSpecs = findDuplicates(data[activeCat]);
if(dupSpecs.size>0){
showToast("處方規格代碼有重複,請先修正");
return;
}
if(!ftpsIsSet(activeCat)){
showToast("尚未設定此類別的 FTPS 連線,請先點「FTPS 設定」填寫");
openFtpsModal();
return;
}
const cat = categories.find(c=>c.key===activeCat);
const f = ftpsSettings[activeCat];
const path = `${ftpsDestString(activeCat)}${cat.file}`;
runModalSequence({
title:`轉換為 XML — ${activeCat}`,
desc:`正在將 ${data[activeCat].length} 筆參數轉換並透過 FTPS 上傳`,
steps:[
"驗證處方規格代碼唯一性...",
"驗證各列必填欄位...",
"驗證類別自動比對規則(客戶/產品設計碼/產品類別)...",
`轉換為 XML 結構(${cat.file})...`,
`建立 FTPS 連線至 ${f.host}:${f.port}...`,
`以帳號「${f.user}」登入並上傳至 ${f.path}${cat.file}...`
],
onDone: ()=>{
const time = new Date().toLocaleString('zh-TW',{hour12:true});
savedState[activeCat] = { time, path };
showModalResult({
title:"已透過 FTPS 上傳",
desc:`${cat.file} 已更新,共 ${data[activeCat].length} 筆參數`,
log:[`✓ 主鍵檢查通過`, `✓ XML 轉換完成`, `✓ 自動比對規則 ${(matchRules[activeCat]||[]).length} 條已生效`, `✓ FTPS 已寫入 ${path}`]
});
editMode = false;
editSnapshot = null;
renderTable();
}
});
});

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

renderTabs();
renderTable();
</script>
</body>
</html>
(3-3/3)