Project

General

Profile

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

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

 
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>MES系統 - 加工參數檔設定(.dat)</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;}
.btn-xs{padding:4px 9px;font-size:11.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);}

.section-row td{
background:#eef2ff;border-bottom:1px solid var(--gray-200);border-top:2px solid var(--blue-500);
padding:9px 10px;
}
.section-row .section-bar{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.section-row .section-name{display:flex;align-items:center;gap:8px;font-weight:700;color:var(--blue-800);font-size:13px;}
.section-row .section-tag{font-size:10.5px;font-weight:700;background:var(--blue-600);color:#fff;padding:2px 8px;border-radius:4px;}
.section-row .section-actions{display:flex;gap:6px;}

.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:disabled{opacity:.35;cursor:not-allowed;}
.icon-btn.plain{color:var(--blue-600);}
.icon-btn.plain:hover{background:#eff6ff;border-color:#bfdbfe;}

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

.empty-state{padding:40px 20px;text-align:center;color:var(--gray-500);font-size:13px;}

.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%;}
.dat-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">加工參數檔設定(.xml)</div>
<div class="nav-sub current">加工參數檔設定(.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">
加工參數檔設定(.dat)
<div class="breadcrumb">設備加工檔 / 加工參數檔設定(.dat)</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>.dat 為「大標籤 [Section] > 小標籤(處方欄位)」的階層結構;小標籤(處方欄位)為主鍵,同一類別內不可重複</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="addSectionBtn" 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="previewDatBtn">🔍 預覽 .dat</button>
<button class="btn btn-green btn-sm" id="saveDatBtn" 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="sectionCount">0</b> 個大標籤 / <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="min-width:180px;">小標籤(處方欄位,主鍵)</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="empty-state" id="emptyState" style="display:none;">此類別尚無任何大標籤,請點「✎ 修改」後再點「+ 新增大標籤」開始建立</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>

<!-- .dat preview modal -->
<div class="modal-overlay" id="datModal">
<div class="modal wide">
<h3 id="datModalTitle">.dat 預覽</h3>
<p id="datModalSub"></p>
<div class="dat-pre" id="datPreviewContent" style="margin-top:14px;"></div>
<button class="btn btn-gray modal-close" id="closeDatBtn">關閉</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 section modal -->
<div class="modal-overlay" id="addSectionModal">
<div class="modal">
<h3>新增大標籤</h3>
<p>建立一個新的 [Section],之後可在其下新增小標籤(處方欄位)</p>
<div style="text-align:left;margin-top:16px;">
<div class="field">
<label>大標籤名稱</label>
<input type="text" id="newSectionName" placeholder="例如 Ortho / BACK / FRONT">
</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
<button class="btn btn-gray" style="flex:1;" id="cancelAddSectionBtn">取消</button>
<button class="btn btn-green" style="flex:1;" id="confirmAddSectionBtn">建立</button>
</div>
</div>
</div>

<!-- Add category modal -->
<div class="modal-overlay" id="addCatModal">
<div class="modal">
<h3>新增類別</h3>
<p>建立一個全新、空白的 .dat 類別,可自行新增大標籤/小標籤並儲存</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="例如 RGP_Basic">
</div>
<div class="field">
<label>檔名 (.dat)</label>
<input type="text" id="newCatFile" placeholder="例如 RGP_Basic.dat">
</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.dat" }
];

// ---- 種子資料:依「A518-2607280001.dat」範例檔的 大標籤[Section] > 小標籤(處方欄位) 結構整理 ----
const seedData = {
"OK": [{"name": "Order", "rows": [{"tag": "Model Code", "dtype": "1:處方規格內容", "spec": "ModelCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 1}, {"tag": "Lot Number", "dtype": "1:處方規格內容", "spec": "LotNo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 2}, {"tag": "SO Number", "dtype": "1:處方規格內容", "spec": "SONo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 3}, {"tag": "Part Number", "dtype": "1:處方規格內容", "spec": "PartNo", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 4}]}, {"name": "Material", "rows": [{"tag": "Material Code", "dtype": "1:處方規格內容", "spec": "MaterialCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 5}]}, {"name": "Measure", "rows": [{"tag": "Measure Code", "dtype": "1:處方規格內容", "spec": "MeasureCode", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 6}]}, {"name": "Data", "rows": [{"tag": "Dia", "dtype": "1:處方規格內容", "spec": "DIA", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 7}, {"tag": "Power", "dtype": "1:處方規格內容", "spec": "Power", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 8}, {"tag": "BC", "dtype": "1:處方規格內容", "spec": "BC", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 9}]}, {"name": "Ortho", "rows": [{"tag": "Target", "dtype": "1:處方規格內容", "spec": "Target", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 10}, {"tag": "FlatK", "dtype": "1:處方規格內容", "spec": "FK", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 11}, {"tag": "Ecc", "dtype": "1:處方規格內容", "spec": "Ecc", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 12}, {"tag": "RCPlus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 13}, {"tag": "AC1Plus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 14}, {"tag": "AC2Plus (Flat)", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 15}, {"tag": "PCR (Flat)", "dtype": "1:處方規格內容", "spec": "PCR_Flat", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 16}, {"tag": "AC2AddRadius", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 17}, {"tag": "FKplusDK", "dtype": "4:處方規格運算", "spec": "", "fixed": "", "op1": "FK", "opMode": "1:加", "op2": "DK", "order": 18}, {"tag": "AC1PlusSt", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 19}, {"tag": "AC2PlusSt", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 20}, {"tag": "PeripheralDefocus", "dtype": "1:處方規格內容", "spec": "Peripheral Defocus", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 21}]}, {"name": "Thickness", "rows": [{"tag": "CenterThickness", "dtype": "1:處方規格內容", "spec": "CT", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 22}, {"tag": "EdgeThickness", "dtype": "1:處方規格內容", "spec": "Edge Th", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 23}]}, {"name": "BACK", "rows": [{"tag": "AZE", "dtype": "1:處方規格內容", "spec": "AZE", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 24}, {"tag": "SteepAZE", "dtype": "1:處方規格內容", "spec": "Steep AZE", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 25}, {"tag": "B BlendFactor", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 26}, {"tag": "EdgeR", "dtype": "3:固定值", "spec": "", "fixed": "0.07", "op1": "", "opMode": "", "op2": "", "order": 27}, {"tag": "BCR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 28}, {"tag": "BOZ", "dtype": "1:處方規格內容", "spec": "OZ", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 29}, {"tag": "BAsp1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 30}, {"tag": "BJR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 31}, {"tag": "BCR2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 32}, {"tag": "BCW2", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 33}, {"tag": "BAsp2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 34}, {"tag": "BJR2", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 35}, {"tag": "BCR3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 36}, {"tag": "BCW3", "dtype": "3:固定值", "spec": "", "fixed": "0.70", "op1": "", "opMode": "", "op2": "", "order": 37}, {"tag": "BAsp3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 38}, {"tag": "BJR3", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 39}, {"tag": "BCR4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 40}, {"tag": "BCW4", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 41}, {"tag": "BAsp4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 42}, {"tag": "BJR4", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 43}]}, {"name": "FRONT", "rows": [{"tag": "F BlendFactor", "dtype": "3:固定值", "spec": "", "fixed": "0.60", "op1": "", "opMode": "", "op2": "", "order": 44}, {"tag": "FOZ", "dtype": "1:處方規格內容", "spec": "FOZ", "fixed": "", "op1": "", "opMode": "", "op2": "", "order": 45}, {"tag": "FAsp1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 46}, {"tag": "FJR1", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 47}, {"tag": "FCW2", "dtype": "3:固定值", "spec": "", "fixed": "0", "op1": "", "opMode": "", "op2": "", "order": 48}, {"tag": "IZT", "dtype": "3:固定值", "spec": "", "fixed": "0.00", "op1": "", "opMode": "", "op2": "", "order": 49}, {"tag": "FJR2", "dtype": "3:固定值", "spec": "", "fixed": "0", "op1": "", "opMode": "", "op2": "", "order": 50}]}]
};

// deep clone as working state
let data = JSON.parse(JSON.stringify(seedData));
let activeCat = "OK";
let savedState = {};

// ---- 編輯鎖定狀態 ----
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("addSectionBtn").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 dupTags = findDuplicateTags(data[activeCat]||[]);
const saveBtn = document.getElementById("saveDatBtn");
saveBtn.style.display = editMode ? "inline-flex" : "none";
saveBtn.disabled = dupTags.size>0;
}

// ---- 每個類別(檔案)各自的 FTPS 上傳設定 ----
let ftpsSettings = {
OK: { host:"ftps.paramsvr.local", port:"21", user:"ftp_ok_dat", pass:"OkDatPass!234", path:"/process_dat/OK/", mode:"explicit" }
};

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

// ---- 類別自動比對規則(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 }
]
};

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

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

const allSpecs = [...new Set(Object.values(seedData).flat().flatMap(sec=>sec.rows).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 secCount = 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">${secCount} 大標籤</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()}.dat` : "";
}
});
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}.dat`; }
if(!file.toLowerCase().endsWith(".dat")){ file += ".dat"; }

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

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

// ---- 新增大標籤 ----
const addSectionModal = document.getElementById("addSectionModal");
document.getElementById("addSectionBtn").addEventListener("click", ()=>{
if(!editMode) return;
document.getElementById("newSectionName").value = "";
addSectionModal.classList.add("show");
setTimeout(()=> document.getElementById("newSectionName").focus(), 50);
});
document.getElementById("cancelAddSectionBtn").addEventListener("click", ()=> addSectionModal.classList.remove("show"));
document.getElementById("confirmAddSectionBtn").addEventListener("click", ()=>{
const name = document.getElementById("newSectionName").value.trim();
if(!name){ showToast("請輸入大標籤名稱"); return; }
const sections = data[activeCat];
if(sections.some(s=>s.name.toLowerCase()===name.toLowerCase())){ showToast(`大標籤「${name}」已存在`); return; }
sections.push({ name, rows: [] });
addSectionModal.classList.remove("show");
renderTable();
showToast(`已新增大標籤「${name}」,可在其下新增小標籤`);
});

// ---- 通用確認刪除 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 findDuplicateTags(sections){
const seen = {};
const dupTags = new Set();
sections.forEach(sec=>{
sec.rows.forEach(r=>{
if(r.tag.trim()!==""){
const key = r.tag.trim().toLowerCase();
seen[key] = (seen[key]||0)+1;
if(seen[key]>1) dupTags.add(r.tag.trim());
}
});
});
return dupTags;
}

function renderTable(){
const cat = categories.find(c=>c.key===activeCat);
document.getElementById("catTitle").textContent = `${activeCat} 加工參數列表 (.dat)`;
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 sections = data[activeCat];
const dupTags = findDuplicateTags(sections);
const lockAttr = editMode ? "" : "disabled";

const body = document.getElementById("rowsBody");
body.innerHTML = "";

if(sections.length===0){
document.getElementById("emptyState").style.display = "block";
} else {
document.getElementById("emptyState").style.display = "none";
sections.forEach((sec, secIdx)=>{
const secTr = document.createElement("tr");
secTr.className = "section-row";
secTr.innerHTML = `
<td colspan="9">
<div class="section-bar">
<div class="section-name"><span class="section-tag">大標籤</span> [${escapeHtml(sec.name)}] <span style="font-weight:400;color:var(--gray-500);font-size:11.5px;">(${sec.rows.length} 個小標籤)</span></div>
<div class="section-actions">
<button class="icon-btn plain" data-sec="${secIdx}" data-action="add-field" ${lockAttr}>+ 新增小標籤</button>
<button class="icon-btn" data-sec="${secIdx}" data-action="delete-section" ${lockAttr}>🗑 刪除大標籤</button>
</div>
</div>
</td>
`;
body.appendChild(secTr);

sec.rows.forEach((r, rowIdx)=>{
const isDup = r.tag.trim()!=="" && dupTags.has(r.tag.trim());
const isType1 = r.dtype.startsWith("1:");
const isType3 = r.dtype.startsWith("3:");
const isType4 = r.dtype.startsWith("4:");
const tr = document.createElement("tr");
tr.dataset.sec = secIdx;
tr.dataset.row = rowIdx;
if(isDup) tr.classList.add("dup");
tr.innerHTML = `
<td class="order-num">${r.order}</td>
<td><input type="text" class="cell-input wide ${isDup?'error':''}" data-role="tag" value="${escapeHtml(r.tag)}" placeholder="例如 Dia / Power / BC" ${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" data-role="spec" value="${escapeHtml(r.spec)}" ${(editMode && isType1)?"":"disabled"} placeholder="${isType1?'例如 DIA / BC / FK':''}"></td>
<td><input type="text" class="cell-input small" data-role="fixed" value="${escapeHtml(r.fixed)}" ${(editMode && isType3)?"":"disabled"}></td>
<td><input type="text" list="specCodes" class="cell-input wide" data-role="op1" value="${escapeHtml(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="${escapeHtml(r.op2)}" ${(editMode && isType4)?"":"disabled"}></td>
<td><button class="icon-btn" data-sec="${secIdx}" data-row="${rowIdx}" data-action="delete-field" title="刪除此小標籤" ${lockAttr}>🗑</button></td>
`;
body.appendChild(tr);
});
});
}

const totalRows = sections.reduce((s,sec)=> s+sec.rows.length, 0);
document.getElementById("sectionCount").textContent = sections.length;
document.getElementById("rowCount").textContent = totalRows;

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

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

// ---- 欄位編輯(文字輸入:僅更新資料 + 輕量驗證,避免整表重繪造成失焦)----
document.getElementById("rowsBody").addEventListener("input", e=>{
if(!editMode) return;
const role = e.target.dataset.role;
if(!role) return;
const tr = e.target.closest("tr");
if(tr.dataset.sec===undefined || tr.dataset.row===undefined) return;
const secIdx = Number(tr.dataset.sec);
const rowIdx = Number(tr.dataset.row);
data[activeCat][secIdx].rows[rowIdx][role] = e.target.value;
if(role==="tag") 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 secIdx = Number(tr.dataset.sec);
const rowIdx = Number(tr.dataset.row);
data[activeCat][secIdx].rows[rowIdx].dtype = e.target.value;
renderTable();
});

document.getElementById("rowsBody").addEventListener("click", e=>{
if(!editMode) return;
const btn = e.target.closest("button[data-action]");
if(!btn) return;
const action = btn.dataset.action;
const secIdx = Number(btn.dataset.sec);

if(action==="add-field"){
const sec = data[activeCat][secIdx];
const totalRows = data[activeCat].reduce((s,x)=> s+x.rows.length, 0);
sec.rows.push({ order: totalRows+1, tag:"", dtype:"1:處方規格內容", spec:"", fixed:"", op1:"", opMode:"", op2:"" });
renderTable();
return;
}
if(action==="delete-section"){
const secName = data[activeCat][secIdx].name;
data[activeCat].splice(secIdx,1);
renumber(data[activeCat]);
renderTable();
showToast(`已刪除大標籤「${secName}」`);
return;
}
if(action==="delete-field"){
const rowIdx = Number(btn.dataset.row);
data[activeCat][secIdx].rows.splice(rowIdx,1);
renumber(data[activeCat]);
renderTable();
showToast("已刪除該小標籤");
return;
}
});

function renumber(sections){
let n = 1;
sections.forEach(sec=> sec.rows.forEach(r=> r.order = n++));
}

function refreshValidationOnly(){
const sections = data[activeCat];
const dupTags = findDuplicateTags(sections);
const body = document.getElementById("rowsBody");
[...body.children].forEach(tr=>{
if(tr.dataset.sec===undefined || tr.dataset.row===undefined) return;
const secIdx = Number(tr.dataset.sec);
const rowIdx = Number(tr.dataset.row);
const r = sections[secIdx].rows[rowIdx];
const isDup = r.tag.trim()!=="" && dupTags.has(r.tag.trim());
tr.classList.toggle("dup", isDup);
const tagInput = tr.querySelector('input[data-role="tag"]');
if(tagInput) tagInput.classList.toggle("error", isDup);
});
const alertBox = document.getElementById("dupAlert");
if(dupTags.size>0){
alertBox.style.display = "block";
alertBox.innerHTML = `<div class="alert alert-error"><span class="ico">⚠️</span><div>發現 ${dupTags.size} 個重複的小標籤(處方欄位):<b>${[...dupTags].join("、")}</b>。小標籤為主鍵,請修正後再儲存,目前無法轉換為 .dat。</div></div>`;
} else {
alertBox.style.display = "none";
alertBox.innerHTML = "";
}
document.getElementById("saveDatBtn").disabled = dupTags.size>0;
}

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

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

// ---- .dat 預覽 ----
function buildDat(catKey){
const sections = data[catKey];
let out = "\n";
sections.forEach(sec=>{
out += `[${sec.name}]\n`;
sec.rows.forEach(r=>{
let valuePreview = "";
if(r.dtype.startsWith("1:")){
valuePreview = r.spec ? `<${escapeDat(r.spec)}>` : "<未設定處方規格代碼>";
} else if(r.dtype.startsWith("3:")){
valuePreview = r.fixed;
} else if(r.dtype.startsWith("4:")){
const modeSym = {"1:加":"+","2:減":"-","3:乘":"×","4:除":"÷"}[r.opMode] || r.opMode || "?";
valuePreview = `<${escapeDat(r.op1)||"?"} ${modeSym} ${escapeDat(r.op2)||"?"}>`;
}
out += ` ${r.tag}: ${valuePreview}\n`;
});
out += "\n";
});
return out;
}
function escapeDat(s){ return s||""; }

const datModal = document.getElementById("datModal");
document.getElementById("previewDatBtn").addEventListener("click", ()=>{
const cat = categories.find(c=>c.key===activeCat);
const totalRows = data[activeCat].reduce((s,sec)=> s+sec.rows.length, 0);
document.getElementById("datModalTitle").textContent = `.dat 預覽 — ${cat.file}`;
document.getElementById("datModalSub").textContent = `共 ${data[activeCat].length} 個大標籤、${totalRows} 個小標籤(僅預覽,尖括號 <> 內為對應規則,尚未寫入 Server)`;
document.getElementById("datPreviewContent").textContent = buildDat(activeCat);
datModal.classList.add("show");
});
document.getElementById("closeDatBtn").addEventListener("click", ()=> datModal.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("saveDatBtn").addEventListener("click", ()=>{
if(!editMode) return;
const dupTags = findDuplicateTags(data[activeCat]);
if(dupTags.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 totalRows = data[activeCat].reduce((s,sec)=> s+sec.rows.length, 0);
const path = `${ftpsDestString(activeCat)}${cat.file}`;
runModalSequence({
title:`轉換為 .dat — ${activeCat}`,
desc:`正在將 ${data[activeCat].length} 個大標籤、${totalRows} 個小標籤轉換並透過 FTPS 上傳`,
steps:[
"驗證小標籤(處方欄位)唯一性...",
"驗證各列必填欄位...",
"驗證類別自動比對規則(客戶/產品設計碼/產品類別)...",
`轉換為 .dat 結構(${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} 個大標籤、${totalRows} 個小標籤`,
log:[`✓ 主鍵檢查通過`, `✓ .dat 轉換完成`, `✓ 自動比對規則 ${(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)