<!DOCTYPE html>
|
<html lang="en">
|
<head>
|
<meta charset="UTF-8">
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Expires" content="0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>NCR Online System v62 (Safe Mode)</title>
|
<style>
|
:root { --primary-color: #2563eb; --success-color: #10b981; --danger-color: #ef4444; --bg-color: #f8fafc; --card-bg: #ffffff; --text-color: #1e293b; --border-color: #cbd5e1; --tab-active-bg: #ffffff; --tab-inactive-bg: #e2e8f0; --readonly-banner: #64748b; }
|
body { font-family: "Microsoft JhengHei", "Segoe UI", Roboto, sans-serif; background-color: var(--bg-color); color: var(--text-color); margin: 0; display: flex; flex-direction: column; align-items: center; min-height: 100vh; }
|
|
/* User Banner (Fixed Top) */
|
.user-banner { width: 100%; background-color: #3b82f6; color: white; text-align: center; padding: 8px 0; font-size: 0.9rem; position: fixed; top: 0; z-index: 1000; display: flex; justify-content: center; align-items: center; gap: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
.backup-info { font-size: 0.75rem; color: #e0f2fe; margin-left: 5px; opacity: 0.9; }
|
|
/* Main Layout */
|
.main-wrapper { width: 98%; max-width: 98%; margin-top: 60px; margin-bottom: 20px; }
|
|
/* Tabs Styling */
|
.tab-nav { display: flex; gap: 5px; margin-bottom: 0; padding: 0 10px; border-bottom: 1px solid var(--border-color); background: #f1f5f9; padding-top: 10px; border-radius: 8px 8px 0 0; }
|
.tab-btn { padding: 10px 20px; border: 1px solid var(--border-color); border-bottom: none; border-radius: 8px 8px 0 0; background-color: var(--tab-inactive-bg); cursor: pointer; font-weight: bold; color: #64748b; transition: 0.2s; font-size: 0.95rem; }
|
.tab-btn:hover { background-color: #cbd5e1; }
|
.tab-btn.active { background-color: var(--tab-active-bg); color: var(--primary-color); border-bottom: 1px solid var(--tab-active-bg); margin-bottom: -1px; z-index: 10; }
|
.tab-hidden { display: none !important; }
|
|
.tab-content { display: none; background: white; padding: 20px; border: 1px solid var(--border-color); border-top: none; border-radius: 0 0 8px 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
.tab-content.active { display: block; animation: fadeIn 0.3s; }
|
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
/* Forms */
|
.form-row { display: flex; gap: 15px; margin-bottom: 15px; flex-wrap: wrap;}
|
.form-group { flex: 1; margin-bottom: 15px; min-width: 150px; }
|
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
input, select, textarea { width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 6px; box-sizing: border-box; }
|
input:disabled { background-color: #e2e8f0; color: #94a3b8; cursor: not-allowed; }
|
|
/* Buttons & Tags */
|
.version-tag { background: rgba(0,0,0,0.2); padding: 2px 6px; border-radius: 4px; font-weight: bold; font-family: monospace; }
|
.lang-select { background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.5); color: white; padding: 2px 5px; border-radius: 4px; font-size: 0.8rem; cursor: pointer; width: auto; }
|
.lang-select option { color: black; }
|
|
.btn-group { display: flex; gap: 10px; margin-top: 15px; justify-content: center; flex-wrap: wrap; }
|
button { padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; color: white; transition: 0.2s; white-space: nowrap;}
|
button:disabled { background-color: #cbd5e1 !important; cursor: not-allowed; }
|
.btn-save { background-color: var(--success-color); }
|
.btn-excel { background-color: #16a34a; }
|
.btn-backup { background-color: #6366f1; }
|
.btn-restore { background-color: #f59e0b; }
|
.btn-refresh { background-color: #64748b; }
|
.btn-reset { background-color: #b91c1c; font-size: 0.8rem; padding: 5px 10px; }
|
|
.btn-edit { background-color: #f59e0b; padding: 5px 10px; font-size: 0.85rem;}
|
.btn-edit:hover { background-color: #d97706; }
|
|
/* Admin Tool Button Style */
|
.btn-admin-tool { background-color: #7c3aed; padding: 5px 10px; font-size: 0.85rem; }
|
.btn-admin-tool:hover { background-color: #6d28d9; }
|
|
.btn-logout { background-color: #ef4444; font-size: 0.75rem; padding: 2px 8px; border-radius: 4px; margin-left: 5px; border: 1px solid white;}
|
.btn-log { background-color: #4b5563; }
|
.btn-copy { background-color: #0ea5e9; font-size: 0.75rem; padding: 4px 8px; margin-left:5px; border-radius:4px; border:none; cursor:pointer; color:white; }
|
.btn-copy:active { transform: translateY(1px); }
|
|
/* Filter Section */
|
.filter-fieldset { border: 1px solid #cbd5e1; border-radius: 8px; padding: 12px 15px; margin-bottom: 15px; background: #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
.filter-legend { font-weight: bold; color: var(--primary-color); padding: 0 8px; font-size: 0.95rem; }
|
|
.filter-search-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
|
.search-main { flex: 2; min-width: 200px; padding: 6px 10px; border-radius: 4px; border: 1px solid #cbd5e1; }
|
.search-small { flex: 1; max-width: 120px; min-width: 80px; padding: 6px 10px; border-radius: 4px; border: 1px solid #cbd5e1; }
|
|
.filter-options-row {
|
display: flex; gap: 20px; align-items: center; flex-wrap: wrap;
|
background: #eff6ff;
|
padding: 8px 12px; border-radius: 6px;
|
border: 1px solid #bfdbfe;
|
font-size: 0.9rem;
|
}
|
.filter-item { display: flex; align-items: center; gap: 8px; }
|
.filter-label-bold { font-weight: bold; color: #1e40af; white-space: nowrap; }
|
.filter-divider-small { width: 1px; height: 15px; background: #93c5fd; }
|
.checkbox-options { display: flex; gap: 10px; }
|
.checkbox-label { display: flex; align-items: center; gap: 3px; cursor: pointer; user-select: none; white-space: nowrap; }
|
.date-range { display: flex; align-items: center; gap: 5px; }
|
.btn-clear-search { background-color: #94a3b8; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.9rem; height: 34px; white-space: nowrap;}
|
.btn-clear-search:hover { background-color: #64748b; }
|
.filter-actions-row { margin-top: 12px; display: flex; justify-content: center; gap: 10px; }
|
|
/* Table & Pagination */
|
.pagination-container { display: flex; justify-content: space-between; align-items: center; margin-top: 10px; padding: 8px 15px; background: #f1f5f9; border-radius: 8px; border: 1px solid #cbd5e1; flex-wrap: wrap; gap: 10px;}
|
.pagination-controls { display: flex; align-items: center; gap: 10px; }
|
.page-btn { background-color: white; border: 1px solid #cbd5e1; color: #334155; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 0.9rem;}
|
.page-btn:disabled { color: #94a3b8; cursor: not-allowed; background-color: #f8fafc; }
|
.page-btn:hover:not(:disabled) { background-color: #e2e8f0; }
|
.page-info { font-size: 0.9rem; color: #475569; font-weight: bold; font-family: monospace;}
|
|
table { width: 100%; border-collapse: collapse; margin-top: 5px; font-size: 0.9rem; }
|
th, td { border: 1px solid var(--border-color); padding: 8px; text-align: left; }
|
th { background-color: #f1f5f9; cursor: pointer; user-select: none; }
|
th:hover { background-color: #e2e8f0; }
|
.sort-icon { font-size: 0.7rem; margin-left: 5px; color: #64748b; }
|
|
.status-open { color: #166534; background: #dcfce7; padding: 2px 6px; border-radius: 4px; border: 1px solid #bbf7d0; font-weight:bold;}
|
.status-closed { color: #475569; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; border: 1px solid #cbd5e1; font-weight:bold;}
|
.status-void { color: #991b1b; background: #fee2e2; padding: 2px 6px; border-radius: 4px; border: 1px solid #fecaca; text-decoration: line-through; font-weight:bold;}
|
.unit-badge { display: inline-block; background: #dbeafe; color: #1e40af; padding: 1px 5px; border-radius: 4px; font-size: 0.8rem; margin: 1px; border: 1px solid #bfdbfe;}
|
.unit-badge-sup { background: #fef3c7; color: #b45309; border: 1px solid #fde68a; }
|
.recurring-badge { font-weight: bold; color: #dc2626; border: 1px solid #fca5a5; padding: 1px 6px; border-radius: 10px; background: #fef2f2; font-size: 0.8rem; }
|
|
/* Network Links & Alert */
|
.network-links-box { background-color: #f0f9ff; border: 1px solid #bae6fd; padding: 10px 15px; border-radius: 8px; margin-bottom: 20px; }
|
.network-links-title { font-weight: bold; color: #0369a1; font-size: 0.95rem; margin-bottom: 8px; }
|
.network-link-item { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 0.9rem; flex-wrap: wrap; }
|
.network-path { font-family: Consolas, monospace; background: white; padding: 2px 6px; border: 1px solid #e2e8f0; border-radius: 4px; color: #334155; user-select: all; }
|
.permission-note { font-size: 0.8rem; color: #dc2626; margin-top: 5px; font-style: italic; }
|
|
#dueAlertSection { display: none; background: #fff1f2; border: 2px solid #e11d48; border-radius: 8px; padding: 15px; margin-top: 15px; margin-bottom: 10px; color: #9f1239; max-height: 200px; overflow-y: auto;}
|
.due-alert-title { font-weight: bold; font-size: 1.1rem; margin-bottom: 10px; }
|
.due-item { margin: 5px 0; border-bottom: 1px dashed #fda4af; padding-bottom: 5px; display: flex; gap: 15px; flex-wrap: wrap; align-items:center;}
|
.due-badge { background: #e11d48; color: white; padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 0.8rem; }
|
|
/* Modals */
|
.modal { display: none; position: fixed; z-index: 2000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(2px); }
|
.modal-content { background-color: #fefefe; margin: 5% auto; padding: 25px; border-radius: 12px; width: 90%; max-width: 600px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); animation: slideDown 0.3s; max-height: 90vh; overflow-y: auto;}
|
@keyframes slideDown { from {transform: translateY(-50px); opacity: 0;} to {transform: translateY(0); opacity: 1;} }
|
.modal-header { font-size: 1.2rem; font-weight: bold; margin-bottom: 15px; color: var(--primary-color); border-bottom: 1px solid #eee; padding-bottom: 10px;}
|
.btn-cancel { background-color: #94a3b8; }
|
|
.checkbox-group-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 10px; margin-top: 5px; background: #f8fafc; padding: 10px; border-radius: 8px; border: 1px solid #e2e8f0; }
|
.checkbox-group-grid .checkbox-label { background: white; border: 1px solid #cbd5e1; padding: 4px 8px; border-radius: 4px; justify-content: center; }
|
|
.result-box { background: #eff6ff; border: 2px dashed var(--primary-color); padding: 15px; text-align: center; border-radius: 8px; margin-top: 20px; }
|
.result-text { font-family: Consolas, monospace; font-size: 1.2rem; color: #1e3a8a; font-weight: bold; margin: 10px 0; }
|
|
/* Backup & Rules */
|
.backup-section { border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; background: #ffffff; }
|
.backup-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; background: #f8fafc; padding: 15px; border-radius: 8px; flex-wrap: wrap; margin-bottom: 10px; }
|
.admin-only-row { background:#fff1f2; border:1px solid #fecaca; }
|
.rule-table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 0.95rem; }
|
.rule-table th, .rule-table td { border: 1px solid #cbd5e1; padding: 10px; text-align: left; }
|
.rule-table th { background-color: #f1f5f9; color: #334155; }
|
.example-block { background-color: #f8fafc; padding: 12px; border-radius: 6px; font-family: Consolas, monospace; color: #475569; margin: 10px 0; font-size: 1rem; border:1px solid #e2e8f0; }
|
|
/* Compact Edit Modal Styles */
|
#editModal .modal-content { padding: 15px 20px; width: 95%; max-width: 650px; }
|
#editModal .form-group { margin-bottom: 8px; }
|
#editModal .form-row { margin-bottom: 8px; gap: 10px; }
|
#editModal label { font-size: 0.85rem; margin-bottom: 2px; }
|
#editModal input, #editModal select { padding: 4px 8px; font-size: 0.9rem; height: 32px; }
|
#editModal textarea { padding: 4px 8px; font-size: 0.9rem; min-height: 50px; }
|
#editModal .checkbox-group-grid { margin-top: 2px; padding: 5px; gap: 5px; grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); }
|
#editModal .checkbox-group-grid .checkbox-label { font-size: 0.8rem; padding: 2px 4px; }
|
#unitCheckboxGroup { max-height: 80px; overflow-y: auto; }
|
|
/* Toggle Header */
|
.toggle-header { cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none; }
|
.toggle-header:hover { opacity: 0.8; }
|
.toggle-icon { font-size: 0.85rem; transition: transform 0.3s ease; margin-left: 10px;}
|
.toggle-content { display: block; margin-top: 10px;}
|
.is-collapsed .toggle-content { display: none; }
|
.is-collapsed .toggle-icon { transform: rotate(-90deg); }
|
.network-links-box.is-collapsed { padding-bottom: 10px; }
|
|
@media (max-width: 768px) {
|
.filter-search-row { flex-direction: column; align-items: stretch; }
|
.search-main, .search-small { max-width: 100%; }
|
.filter-options-row { flex-direction: column; align-items: flex-start; gap: 10px; }
|
.pagination-container { flex-direction: column; align-items: flex-start; }
|
.tab-btn { padding: 10px; font-size: 0.8rem; }
|
}
|
</style>
|
</head>
|
<body>
|
|
<div class="user-banner">
|
<span id="currentUserDisplay">User: ...</span>
|
<button class="btn-logout" onclick="logout()" data-i18n="btn_logout">Logout</button>
|
<span class="version-tag">v62</span>
|
<div style="display:flex; flex-direction:column; align-items:center;">
|
<select class="lang-select" id="langSwitcher" onchange="changeLanguage(this.value)">
|
<option value="en">EN</option>
|
<option value="tw">繁中</option>
|
<option value="cn">简中</option>
|
<option value="vi">VN</option>
|
</select>
|
<span id="backupTimeDisplay" class="backup-info">...</span>
|
</div>
|
</div>
|
|
<div class="main-wrapper">
|
<div class="tab-nav">
|
<button class="tab-btn" id="tabBtnRules" onclick="openTab('tab-rules')" data-i18n="tab_rules">1. Rules</button>
|
<button class="tab-btn active" id="tabBtnGen" onclick="openTab('tab-generator')" data-i18n="tab_generator">2. Generator</button>
|
<button class="tab-btn" id="tabBtnHist" onclick="openTab('tab-history')" data-i18n="tab_history">3. History & Search</button>
|
<button class="tab-btn" id="tabBtnAdmin" onclick="openTab('tab-admin')" data-i18n="tab_admin">4. Admin & Backup</button>
|
</div>
|
|
<div id="tab-rules" class="tab-content">
|
<h3 style="margin-top:0; color: var(--primary-color);" data-i18n="header_rules">NCR Naming Rules</h3>
|
<div style="padding:10px;">
|
<h4 data-i18n="rule_format_title" style="color:#475569;">1. Standard Format</h4>
|
<div class="example-block" data-i18n="rule_format_content">[Category]-[Site][YM][Seq]-[Model]-[Desc]-[CustCode]-[Date]</div>
|
|
<h4 data-i18n="rule_def_title" style="color:#475569;">2. Definitions</h4>
|
<table class="rule-table">
|
<thead><tr><th data-i18n="th_field">Field</th><th data-i18n="th_example">Example</th><th data-i18n="th_desc">Desc</th></tr></thead>
|
<tbody>
|
<tr><td><strong data-i18n="label_category">Category</strong></td><td>NCR / INCR</td><td data-i18n="rule_cat_desc">NCR: General; INCR: Internal</td></tr>
|
<tr><td><strong data-i18n="label_site">Site</strong></td><td>TW / HT / VN</td><td data-i18n="rule_site_desc">Taipei/Suzhou/Vietnam</td></tr>
|
<tr><td><strong data-i18n="label_month">Month</strong></td><td>YYYYMM</td><td data-i18n="rule_month_desc">Issue Month (6 digits)</td></tr>
|
<tr><td><strong data-i18n="label_seq">Sequence</strong></td><td>001~999</td><td data-i18n="rule_seq_desc">Reset monthly (3 digits)</td></tr>
|
<tr><td><strong data-i18n="label_model">Model</strong></td><td>...</td><td data-i18n="rule_model_desc">Custom, Optional</td></tr>
|
<tr><td><strong data-i18n="label_custom_name">Custom Name</strong></td><td>Text</td><td data-i18n="rule_cn_desc">Brief desc, Required</td></tr>
|
<tr><td><strong data-i18n="label_cust_code">Cust. Code</strong></td><td>...</td><td data-i18n="rule_cust_desc">Custom, Optional</td></tr>
|
<tr><td><strong data-i18n="label_file_date">Date</strong></td><td>YYYYMMDD</td><td data-i18n="rule_date_desc">Optional, at the end</td></tr>
|
</tbody>
|
</table>
|
</div>
|
</div>
|
|
<div id="tab-generator" class="tab-content active">
|
<h2 style="text-align: center; color: var(--primary-color); margin-top:0;" data-i18n="app_title">NCR Naming System</h2>
|
<div id="generatorForm">
|
<div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_category">Category</label>
|
<select id="category" onchange="updateSequenceDisplayOnly()">
|
<option value="NCR" data-i18n="opt_ncr">NCR</option>
|
<option value="INCR" data-i18n="opt_incr">INCR</option>
|
</select>
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_site">Site</label>
|
<select id="site" onchange="updateSequenceDisplayOnly()">
|
<option value="TW" data-i18n="opt_tw">TW</option>
|
<option value="HT" data-i18n="opt_ht">HT</option>
|
<option value="VN" data-i18n="opt_vn">VN</option>
|
</select>
|
</div>
|
</div>
|
|
<div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_month">Month (YYYYMM)</label>
|
<input type="month" id="yearMonth" onchange="updateSequenceDisplayOnly()">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_seq">Sequence (Preview)</label>
|
<input type="text" id="sequence" readonly placeholder="..." style="background-color:#f1f5f9; color:#64748b; font-weight:bold;">
|
</div>
|
</div>
|
|
<div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_model">Model (Opt.)</label>
|
<input type="text" id="modelName" placeholder="..." oninput="previewName()">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_cust_code">Cust. Code (Opt.)</label>
|
<input type="text" id="customerCode" placeholder="..." oninput="previewName()">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_file_date">Date (Opt.)</label>
|
<input type="text" id="fileDate" placeholder="YYYYMMDD" oninput="previewName()">
|
</div>
|
</div>
|
|
<div class="form-row">
|
<div class="form-group" style="flex:3;">
|
<label><span data-i18n="label_custom_name">Description</span> <span style="color:red">*</span></label>
|
<input type="text" id="customName" placeholder="" oninput="previewName()">
|
</div>
|
<div class="form-group" style="display:flex; align-items:center; margin-top:25px;">
|
<label style="cursor:pointer; display:flex; align-items:center; gap:5px;">
|
<input type="checkbox" id="recurring">
|
<span data-i18n="label_recurring">Recurring Issue</span>
|
</label>
|
</div>
|
</div>
|
|
<div class="result-box">
|
<div id="finalName" class="result-text" data-i18n="msg_fill_data">Please fill data...</div>
|
<div class="btn-group">
|
<button class="btn-refresh" onclick="location.reload()" data-i18n="btn_refresh">Refresh</button>
|
<button class="btn-save" id="btnSave" onclick="saveData()" style="display:none;" data-i18n="btn_confirm">✅ Confirm & Save</button>
|
</div>
|
<div id="saveSuccessMsg" style="display:none; margin-top:15px; padding:10px; background:#dcfce7; color:#166534; border:1px solid #86efac; border-radius:6px; text-align:center; font-weight:bold;"></div>
|
</div>
|
</div>
|
</div>
|
|
<div id="tab-history" class="tab-content">
|
<h3 data-i18n="header_history" style="margin-top:0;">📋 Numbering History</h3>
|
|
<div class="network-links-box is-collapsed" id="networkBox">
|
<div class="network-links-title toggle-header" onclick="toggleSection('networkBox')">
|
<div style="display:flex; align-items:center; gap:5px;">
|
📂 <span data-i18n="title_network_links">Network Shared Folders</span>
|
</div>
|
<span class="toggle-icon">▼</span>
|
</div>
|
|
<div class="toggle-content">
|
<div class="network-link-item">
|
<strong style="width:120px;" data-i18n="link_name_closed">Archive:</strong>
|
<span class="network-path">\\10.192.130.229\Area51\NCRDCC</span>
|
<button class="btn-copy" onclick="copyToClipboard('\\\\10.192.130.229\\Area51\\NCRDCC')" data-i18n="btn_copy">Copy Path</button>
|
</div>
|
<div class="network-link-item">
|
<strong style="width:120px;" data-i18n="link_name_ht">HT Work Area:</strong>
|
<span class="network-path">\\10.176.5.233\公司部门\蘇州週報區\01 NCR+客诉 清单---不可剪切</span>
|
<button class="btn-copy" onclick="copyToClipboard('\\\\10.176.5.233\\公司部门\\蘇州週報區\\01 NCR+客诉 清单---不可剪切')" data-i18n="btn_copy">Copy Path</button>
|
</div>
|
<div class="network-link-item">
|
<strong style="width:120px;" data-i18n="link_name_vn">VN Work Area:</strong>
|
<span class="network-path">\\10.192.140.227\vn\department\QC\-1.不良事件追蹤(NCR, INCR)Theo dõi biến cố bất lợi</span>
|
<button class="btn-copy" onclick="copyToClipboard('\\\\192.1.2.227\\vn\\department\\QC\\-1.不良事件追蹤(NCR, INCR)Theo dõi biến cố bất lợi')" data-i18n="btn_copy">Copy Path</button>
|
</div>
|
<div class="network-link-item">
|
<strong style="width:120px;" data-i18n="link_name_tw">TW Work Area:</strong>
|
<span class="network-path">\\10.192.130.229\公司部門\品保處\1. NCR</span>
|
<button class="btn-copy" onclick="copyToClipboard('\\\\10.192.130.229\\taipei\\公司部門\\品保處\\1. NCR')" data-i18n="btn_copy">Copy Path</button>
|
</div>
|
<div class="permission-note" data-i18n="note_permission">Note: You need IT permission to access these folders. Please copy path and paste in File Explorer.</div>
|
</div>
|
</div>
|
|
<fieldset class="filter-fieldset">
|
<legend class="filter-legend" data-i18n="label_filter_title">Advanced Search</legend>
|
|
<div class="filter-search-row">
|
<input type="text" id="searchInput" class="search-main" placeholder="Keyword (File, Desc, ID...)" oninput="applyFilterAndSort()">
|
<input type="text" id="searchModel" class="search-small" placeholder="Model" oninput="applyFilterAndSort()">
|
<input type="text" id="searchCustCode" class="search-small" placeholder="Cust. Code" oninput="applyFilterAndSort()">
|
<label class="checkbox-label" style="color:#dc2626; font-weight:bold; margin-left:5px;">
|
<input type="checkbox" id="filterRecurring" onchange="applyFilterAndSort()">
|
<span data-i18n="label_filter_recurring_only">Recurring Only</span>
|
</label>
|
<button class="btn-clear-search" onclick="clearSearch()" data-i18n="btn_clear">Clear</button>
|
</div>
|
|
<div class="filter-options-row">
|
<div class="filter-item">
|
<span class="filter-label-bold" data-i18n="label_filter_category">Cat:</span>
|
<div class="checkbox-options">
|
<label class="checkbox-label"><input type="checkbox" name="filterCategory" value="NCR" onchange="applyFilterAndSort()"> NCR</label>
|
<label class="checkbox-label"><input type="checkbox" name="filterCategory" value="INCR" onchange="applyFilterAndSort()"> INCR</label>
|
</div>
|
</div>
|
|
<div class="filter-divider-small"></div>
|
|
<div class="filter-item">
|
<span class="filter-label-bold" data-i18n="label_filter_site">Site:</span>
|
<div class="checkbox-options">
|
<label class="checkbox-label"><input type="checkbox" name="filterSite" value="TW" onchange="applyFilterAndSort()"> TW</label>
|
<label class="checkbox-label"><input type="checkbox" name="filterSite" value="HT" onchange="applyFilterAndSort()"> HT</label>
|
<label class="checkbox-label"><input type="checkbox" name="filterSite" value="VN" onchange="applyFilterAndSort()"> VN</label>
|
</div>
|
</div>
|
|
<div class="filter-divider-small"></div>
|
|
<div class="filter-item">
|
<span class="filter-label-bold" data-i18n="label_filter_status">Status:</span>
|
<div class="checkbox-options">
|
<label class="checkbox-label"><input type="checkbox" name="filterStatus" value="Open" onchange="applyFilterAndSort()"> <span data-i18n="status_open">Open</span></label>
|
<label class="checkbox-label"><input type="checkbox" name="filterStatus" value="Closed" onchange="applyFilterAndSort()"> <span data-i18n="status_closed">Closed</span></label>
|
<label class="checkbox-label"><input type="checkbox" name="filterStatus" value="Void" onchange="applyFilterAndSort()"> <span data-i18n="status_void">Void</span></label>
|
</div>
|
</div>
|
|
<div class="filter-divider-small"></div>
|
|
<div class="filter-item">
|
<span class="filter-label-bold" data-i18n="label_filter_date">Date:</span>
|
<div class="date-range">
|
<input type="date" id="searchStart" onchange="applyFilterAndSort()" style="width:125px; padding:4px;">
|
<span>~</span>
|
<input type="date" id="searchEnd" onchange="applyFilterAndSort()" style="width:125px; padding:4px;">
|
</div>
|
</div>
|
</div>
|
|
<div class="filter-actions-row">
|
<button class="btn-refresh" onclick="loadHistory()" data-i18n="btn_refresh_list">🔄 Refresh Data</button>
|
<button class="btn-excel" onclick="exportExcel()" data-i18n="btn_export">📊 Export Filtered Excel</button>
|
</div>
|
</fieldset>
|
|
<div id="dueAlertSection" style="display:none;" class="is-collapsed">
|
<div class="due-alert-title toggle-header" onclick="toggleSection('dueAlertSection')">
|
<div style="display:flex; align-items:center; gap:10px;">
|
⚠️ <span data-i18n="header_due_alert">Overdue Alert</span>
|
</div>
|
<span class="toggle-icon">▼</span>
|
</div>
|
<div id="dueAlertContent" class="toggle-content"></div>
|
</div>
|
|
<div class="pagination-container">
|
<div class="pagination-controls">
|
<span data-i18n="label_rows_per_page">Rows:</span>
|
<select id="rowsPerPage" onchange="changeRowsPerPage()" style="width:auto; padding:4px;">
|
<option value="20">20</option>
|
<option value="50">50</option>
|
<option value="100">100</option>
|
<option value="999999">All</option>
|
</select>
|
<span class="page-info" id="pageInfoDisplay">Showing 0-0 of 0</span>
|
</div>
|
<div class="pagination-controls">
|
<button class="page-btn" id="btnPrevPage" onclick="prevPage()" data-i18n="btn_prev">Prev</button>
|
<button class="page-btn" id="btnNextPage" onclick="nextPage()" data-i18n="btn_next">Next</button>
|
</div>
|
</div>
|
|
<div style="overflow-x: auto;">
|
<table id="historyTable">
|
<thead>
|
<tr>
|
<th onclick="sortBy('fullFileName')"><span data-i18n="th_filename">Filename</span> <span id="sort_fullFileName" class="sort-icon"></span></th>
|
<th onclick="sortBy('status')" style="width:70px; text-align:center;"><span data-i18n="th_status">Status</span> <span id="sort_status" class="sort-icon"></span></th>
|
<th onclick="sortBy('unit')" style="width:150px;"><span data-i18n="th_unit">Resp. Unit</span> <span id="sort_unit" class="sort-icon"></span></th>
|
|
<th onclick="sortBy('model')" style="width:80px;"><span data-i18n="th_model">Model</span> <span id="sort_model" class="sort-icon"></span></th>
|
<th onclick="sortBy('customerCode')" style="width:80px;"><span data-i18n="th_cust_code">Cust. Code</span> <span id="sort_customerCode" class="sort-icon"></span></th>
|
<th onclick="sortBy('complaintId')" style="width:100px;"><span data-i18n="th_complaint_id">Complaint ID</span> <span id="sort_complaintId" class="sort-icon"></span></th>
|
<th onclick="sortBy('calculatedRate')" style="width:80px;"><span data-i18n="th_defect_rate">Rate(%)</span> <span id="sort_calculatedRate" class="sort-icon"></span></th>
|
<th onclick="sortBy('penaltyDate')" style="width:100px;"><span data-i18n="th_penalty_date">Penalty Start</span> <span id="sort_penaltyDate" class="sort-icon"></span></th>
|
|
<th onclick="sortBy('recurring')" style="width:50px; text-align:center;"><span data-i18n="th_recurring">Recur</span> <span id="sort_recurring" class="sort-icon"></span></th>
|
<th onclick="sortBy('closingDate')" style="width:110px;"><span data-i18n="th_closing_date">Closing Date</span> <span id="sort_closingDate" class="sort-icon">▼</span></th>
|
|
<th style="width:170px;" data-i18n="th_action">Action</th>
|
</tr>
|
</thead>
|
<tbody id="historyBody"></tbody>
|
</table>
|
</div>
|
</div>
|
|
<div id="tab-admin" class="tab-content">
|
<h3 style="margin-top:0;" data-i18n="header_backup">💾 Database Management</h3>
|
<div class="backup-section">
|
<div class="backup-row">
|
<div>
|
<strong data-i18n="label_backup">1. Backup</strong>
|
<p style="margin:5px 0 0 0; font-size:0.85rem; color:#64748b;" data-i18n="desc_backup">Download JSON file.</p>
|
</div>
|
<div style="display:flex; gap:10px;">
|
<button class="btn-backup" onclick="downloadBackup()" data-i18n="btn_download_backup">⬇️ Download DB</button>
|
</div>
|
</div>
|
<div class="backup-row">
|
<div>
|
<strong data-i18n="label_restore">2. Restore</strong>
|
<p style="margin:5px 0 0 0; font-size:0.85rem; color:#64748b;" data-i18n="desc_restore">Upload JSON (Merge mode).</p>
|
</div>
|
<div class="file-upload">
|
<input type="file" id="restoreFile" accept=".json">
|
<button id="btnRestore" class="btn-restore" onclick="uploadRestore()" data-i18n="btn_upload_restore">⬆️ Restore</button>
|
</div>
|
</div>
|
<div class="backup-row admin-only-row">
|
<div>
|
<strong style="color:#b91c1c;">3. Admin Zone</strong>
|
<p style="margin:5px 0 0 0; font-size:0.85rem; color:#7f1d1d;">Dangerous actions.</p>
|
</div>
|
<div style="display:flex; gap:10px;">
|
<button class="btn-log" onclick="downloadLogs()" data-i18n="btn_export_logs" style="color:white; padding:8px 15px; border-radius:6px; border:none; cursor:pointer;">📜 Export Logs</button>
|
<button id="btnResetDB" class="btn-reset" onclick="resetDatabase()" data-i18n="btn_reset_db">🗑️ Clear DB</button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div id="editModal" class="modal">
|
<div class="modal-content">
|
<div class="modal-header" data-i18n="modal_title">Edit Record</div>
|
<input type="hidden" id="editUniqueId">
|
<input type="hidden" id="editVersion"> <div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_status">Status</label>
|
<select id="editStatus" onchange="toggleClosingDateInput()">
|
<option value="Open" data-i18n="status_open">Open</option>
|
<option value="Closed" data-i18n="status_closed">Closed</option>
|
<option value="Void" data-i18n="status_void">Void</option>
|
</select>
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_closing_date">Closing Date</label>
|
<input type="date" id="editClosingDate" disabled>
|
</div>
|
</div>
|
|
<div class="form-row" style="align-items: flex-end;">
|
<div class="form-group" style="flex:1;">
|
<label data-i18n="label_complaint_id">Complaint ID</label>
|
<input type="text" id="editComplaintId" placeholder="...">
|
</div>
|
|
<div class="form-group" style="display:flex; align-items:center; justify-content: space-between; gap:5px; padding-bottom:0; flex: 1.2;">
|
<label style="cursor:pointer; display:flex; align-items:center; gap:5px; margin-bottom:0;">
|
<input type="checkbox" id="editRecurring">
|
<span data-i18n="label_recurring" style="white-space:nowrap;">Recurring</span>
|
</label>
|
|
<div style="display:flex; gap:10px; margin-left: auto;">
|
<button class="btn-cancel" onclick="closeEditModal()" data-i18n="btn_cancel" style="padding:4px 10px; font-size:0.85rem;">Cancel</button>
|
<button class="btn-save" onclick="saveEditModal()" data-i18n="btn_save_changes" style="padding:4px 10px; font-size:0.85rem;">Save</button>
|
</div>
|
</div>
|
</div>
|
|
<div class="form-row" style="margin-top:5px; border-top:1px dashed #cbd5e1; padding-top:5px;">
|
<div class="form-group">
|
<label data-i18n="label_return_qty">Return Qty</label>
|
<input type="number" id="editReturnQty" placeholder="0">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_shipped_qty">Shipped Qty</label>
|
<input type="number" id="editShippedQty" placeholder="0">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_threshold">Alert Threshold (%)</label>
|
<input type="number" id="editDefectThreshold" step="0.01" placeholder="0.0">
|
</div>
|
</div>
|
|
<div class="form-row" style="align-items: flex-end;">
|
<div class="form-group" style="flex:1;">
|
<label data-i18n="label_penalty_date">Penalty Start</label>
|
<input type="date" id="editPenaltyDate">
|
</div>
|
<div class="form-group" style="flex:1;">
|
<label data-i18n="label_grace_period">Grace (Days)</label>
|
<input type="number" id="editGracePeriod" placeholder="0">
|
</div>
|
<div class="form-group" style="flex:0.8; padding-bottom: 5px;">
|
<label style="cursor:pointer; display:flex; align-items:center; gap:5px; color:#64748b; font-size:0.85rem; white-space:nowrap; margin-bottom:0;">
|
<input type="checkbox" id="editStopAlert">
|
<span data-i18n="label_stop_alert">Stop Reminding</span>
|
</label>
|
</div>
|
</div>
|
|
<div class="form-group">
|
<label data-i18n="label_resp_unit">Responsible Unit (Multiple)</label>
|
<div class="checkbox-group-grid" id="unitCheckboxGroup"></div>
|
|
<div style="margin-top:5px; padding:5px; background:#f0f9ff; border:1px solid #bae6fd; border-radius:6px; display:flex; align-items:center; gap:10px;">
|
<label style="display:flex; align-items:center; gap:5px; cursor:pointer; margin:0; font-size:0.85rem;">
|
<input type="checkbox" id="editChkSup" onchange="toggleEditSupInput()">
|
<span data-i18n="unit_sup" style="font-weight:bold; color:#0369a1;">Supplier</span>
|
</label>
|
<input type="text" id="editTxtSup" placeholder="Name..." style="flex:1; display:none; border:1px solid #0ea5e9; height:28px;">
|
</div>
|
</div>
|
<div class="form-group" style="margin-bottom:0;">
|
<label data-i18n="label_remarks">Remarks</label>
|
<textarea id="editRemarks" rows="2"></textarea>
|
</div>
|
</div>
|
</div>
|
|
<div id="adminModal" class="modal">
|
<div class="modal-content" style="border: 2px solid #7c3aed;">
|
<div class="modal-header" style="color: #7c3aed;">
|
<span data-i18n="modal_admin_title">🔧 Admin Metadata Edit</span>
|
</div>
|
<input type="hidden" id="adminUniqueId">
|
<input type="hidden" id="adminVersion"> <div class="form-row">
|
<div class="form-group">
|
<label style="color:#64748b;">Unique ID</label>
|
<input type="text" id="adminDisplayId" style="font-weight:bold;" oninput="previewAdminFilename()">
|
</div>
|
</div>
|
|
<div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_custom_name">Description</label> <span style="color:red">*</span>
|
<input type="text" id="adminCustomName">
|
</div>
|
</div>
|
|
<div class="form-row">
|
<div class="form-group">
|
<label data-i18n="label_model">Model</label>
|
<input type="text" id="adminModel">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_cust_code">Cust. Code</label>
|
<input type="text" id="adminCustCode">
|
</div>
|
<div class="form-group">
|
<label data-i18n="label_file_date">Date Suffix</label>
|
<input type="text" id="adminFileDate" placeholder="YYYYMMDD">
|
</div>
|
</div>
|
|
<div class="result-box" style="margin: 10px 0; padding: 10px; background: #f5f3ff; border-color: #7c3aed;">
|
<div style="font-size:0.8rem; color:#7c3aed; margin-bottom:5px;">Preview New Filename:</div>
|
<div id="adminPreviewName" class="result-text" style="font-size:1rem;">...</div>
|
</div>
|
|
<div style="display:flex; justify-content: flex-end; gap:10px; margin-top:20px;">
|
<button class="btn-cancel" onclick="closeAdminModal()" data-i18n="btn_cancel">Cancel</button>
|
<button class="btn-admin-tool" onclick="saveAdminModal()" data-i18n="btn_save_changes">Update Metadata</button>
|
</div>
|
</div>
|
</div>
|
|
<div id="fileModal" class="modal">
|
<div class="modal-content" style="border: 2px solid #0ea5e9; max-width: 550px; padding: 20px;">
|
<div class="modal-header" style="color: #0ea5e9; border-bottom: 1px solid #eee; padding-bottom: 10px;">
|
<span data-i18n="modal_file_title">📁 關聯檔案設定</span>
|
</div>
|
<input type="hidden" id="fileUniqueId">
|
<input type="hidden" id="fileVersion">
|
|
<div class="form-group" style="margin-top: 15px;">
|
<label data-i18n="label_file_path">完整路徑與檔案名稱</label>
|
<div style="font-size: 0.8rem; color: #64748b; margin-bottom: 5px;">
|
💡 提示:在 Windows 檔案總管中,對著檔案「<b>按住 Shift 鍵 + 按滑鼠右鍵</b>」,選擇「<b>複製為路徑 (A)</b>」即可快速取得含檔名的路徑。
|
</div>
|
<input type="text" id="fileInputPath" placeholder="例如: \\10.192.130.229\taipei\abc.xlsx 或 C:\Folder\file.pdf">
|
</div>
|
<div id="filePreviewContainer" style="margin: 15px 0; padding: 10px; background: #f0f9ff; border: 1px dashed #0ea5e9; border-radius: 6px; display: none;">
|
<div style="font-size:0.8rem; color:#0ea5e9; margin-bottom:5px; font-weight:bold;" data-i18n="label_current_link">目前關聯檔案 (點選可開啟):</div>
|
<div id="filePreviewLink"></div>
|
</div>
|
|
<div style="display:flex; justify-content: flex-end; gap:10px; margin-top:20px;">
|
<button class="btn-cancel" onclick="closeFileModal()" data-i18n="btn_cancel">Cancel</button>
|
<button class="btn-reset" onclick="clearFilePath()" style="background-color: #ef4444;" data-i18n="btn_clear_path">清除路徑</button>
|
<button class="btn-save" style="background-color: #0ea5e9; color:white; font-weight:bold;" onclick="saveFileModal()" data-i18n="btn_save_changes">Save</button>
|
</div>
|
</div>
|
</div>
|
|
<script>
|
const RESP_UNITS = [
|
{ code: 'MGMT', key: 'unit_mgmt' }, { code: 'ENG', key: 'unit_eng' }, { code: 'PMC', key: 'unit_pmc' },
|
{ code: 'QA', key: 'unit_qa' }, { code: 'PC', key: 'unit_pc' }, { code: 'MFG', key: 'unit_mfg' },
|
{ code: 'PUR', key: 'unit_pur' }, { code: 'RD', key: 'unit_rd' }, { code: 'SALES', key: 'unit_sales' },
|
{ code: 'RA', key: 'unit_ra' }
|
];
|
|
const translations = {
|
en: {
|
app_title: "NCR Naming System", status_connecting: "Connecting...",
|
status_readonly: "🔒 Read-Only Mode",
|
label_category: "Category", label_site: "Site", label_month: "Month (YYYYMM)", label_seq: "Sequence", label_custom_name: "Description",
|
opt_ncr: "NCR (Non-Conformance Report)", opt_incr: "INCR (Internal Use)",
|
opt_tw: "TW (Taipei HQ)", opt_ht: "HT (Suzhou Plant)", opt_vn: "VN (Vietnam Plant)",
|
msg_fill_data: "Please fill data...", btn_refresh: "Refresh", btn_confirm: "✅ Confirm & Save",
|
header_history: "📋 Numbering History", btn_refresh_list: "🔄 Refresh Data", btn_export: "📊 Filtered Excel",
|
th_filename: "Filename", th_status: "Status", th_unit: "Resp. Unit", th_time: "Time", th_action: "Action",
|
header_backup: "💾 Database Management", label_backup: "1. Backup", desc_backup: "Download JSON file.", btn_download_backup: "⬇️ Download DB",
|
label_restore: "2. Restore", desc_restore: "Upload JSON (Merge mode).", btn_upload_restore: "⬆️ Restore",
|
btn_reset_db: "🗑️ Clear DB", btn_clear: "Clear", btn_logout: "Logout", btn_close: "Close",
|
btn_export_logs: "📜 Export Logs",
|
last_backup: "Backup: ",
|
|
label_filter_title: "Advanced Search", label_filter_category: "Category", label_filter_site: "Site", label_filter_status: "Status", label_filter_date: "Date Range", label_filter_keyword: "Keyword",
|
|
prompt_name: "Please enter your name:", user_label: "User: ",
|
msg_conn_fail: "⚠️ Cannot connect to server",
|
placeholder_custom: "e.g. Scratch on case", placeholder_search: "Keyword (File, Desc, ID...)",
|
alert_success: "✅ Saved Successfully!", alert_fail: "❌ Failed: ", alert_id_exist: "🚫 ID Already Exists!",
|
|
modal_title: "Edit Record", label_status: "Status", label_resp_unit: "Responsible Unit (Multiple)", label_remarks: "Remarks",
|
btn_cancel: "Cancel", btn_save_changes: "Save", btn_edit: "✏️ Edit", btn_detail: "📄 Detail",
|
status_open: "Open", status_closed: "Closed", status_void: "Void",
|
unit_mgmt: "MGMT", unit_eng: "ENG", unit_pmc: "PMC", unit_qa: "QA", unit_pc: "PC", unit_mfg: "MFG", unit_pur: "PUR", unit_rd: "RD", unit_sales: "SALES",
|
unit_ra: "RA (Regulatory)", // 新增
|
unit_sup: "Supplier",
|
msg_confirm_reset: "⚠️ DANGER: This will delete ALL data!\nAre you sure?", msg_reset_ok: "Database cleared.", msg_perm_denied: "⛔ Permission Denied (Admins only).",
|
|
header_rules: "NCR Naming Rules", rule_format_title: "1. Standard Format", rule_format_content: "[Category]-[Site][YM][Seq]-[Model]-[Desc]-[CustCode]-[Date]",
|
rule_def_title: "2. Definitions", th_field: "Field", th_example: "Example", th_desc: "Description",
|
rule_cat_desc: "INCR is for internal use", rule_site_desc: "TW(Taipei), HT(Suzhou), VN(Vietnam)",
|
rule_month_desc: "Issue Month (YYYYMM)", rule_seq_desc: "Reset monthly, Unique", rule_cn_desc: "Brief description, required",
|
rule_model_desc: "Product Model (Optional)", rule_cust_desc: "Customer Code (Optional)", rule_date_desc: "Date suffix (Optional)",
|
|
label_rows_per_page: "Rows:", btn_prev: "Prev", btn_next: "Next",
|
|
label_cust_code: "Cust. Code (Opt.)", label_recurring: "Recurring", label_model: "Model (Opt.)", label_file_date: "Date (Opt.)",
|
th_cust_code: "Cust. Code", th_recurring: "Recur", th_model: "Model",
|
label_filter_recurring_only: "Recurring Only", label_complaint_id: "Complaint ID", th_complaint_id: "Complaint ID",
|
|
label_penalty_date: "Penalty Start Date", label_grace_period: "Grace Period (Days)", th_penalty_date: "Penalty Start", header_due_alert: "⚠️ Overdue Alert",
|
alert_penalty_started: "Penalty Active: {0} days", label_stop_alert: "Stop Reminding",
|
label_return_qty: "Return Qty", label_shipped_qty: "Shipped Qty", label_threshold: "Alert Threshold (%)",
|
th_defect_rate: "Rate(%)", alert_defect_rate: "High Defect Rate ({0}%)",
|
|
tab_rules: "1. Rules", tab_generator: "2. Generator", tab_history: "3. History & Search", tab_admin: "4. Admin & Backup",
|
role_admin: "Admin", role_editor: "Editor (History Only)", role_viewer: "Read-Only", role_guest: "Guest (Rules Only)",
|
|
msg_gen_readonly: "🔒 Read-Only Mode",
|
msg_gen_editor: "⚠️ Editor: History Edit Only",
|
msg_gen_guest: "⛔ Restricted Access: Rules Only",
|
label_limit: "Limit",
|
|
title_network_links: "Network Shared Folders", link_name_closed: "Archive:", link_name_ht: "HT Work Area:", link_name_vn: "VN Work Area:", link_name_tw: "TW Work Area:",
|
btn_copy: "Copy Path", note_permission: "Note: You need IT permission to access these folders. Please copy path and paste in File Explorer.",
|
msg_copy_ok: "Path copied! Please paste it in your File Explorer.", msg_copy_fail: "Copy failed. Please select and copy manually.",
|
|
label_closing_date: "Closing Date", th_closing_date: "Closing Date",
|
// Admin New Translations
|
btn_admin_tool: "🔧 Admin",
|
modal_admin_title: "🔧 Admin Metadata Edit",
|
msg_admin_saved: "✅ Metadata Updated! Filename changed.",
|
msg_data_conflict: "⚠️ DATA CONFLICT!\n\nThis record has been modified by another user.\nSystem blocked your save to prevent data loss.\n\nPlease refresh the page and try again.",
|
// 關聯檔案新翻譯
|
btn_file_link: "File Link",
|
modal_file_title: "📁 Associated File Settings",
|
label_file_path: "Full Path and Filename",
|
label_current_link: "Current Attached File (Click to open):",
|
btn_clear_path: "Clear Path",
|
msg_file_saved: "✅ File path saved successfully!"
|
},
|
tw: {
|
app_title: "NCR 命名生成器", status_connecting: "連線中...",
|
status_readonly: "🔒 唯讀瀏覽模式",
|
label_category: "文件類別", label_site: "廠區碼", label_month: "開單月份 (YYYYMM)", label_seq: "流水號", label_custom_name: "自訂名稱",
|
opt_ncr: "NCR (一般報告)", opt_incr: "INCR (內部使用報告 - 不外傳)",
|
opt_tw: "TW (台北總部)", opt_ht: "HT (蘇州合泰廠)", opt_vn: "VN (越南廠)",
|
msg_fill_data: "請填寫資料...", btn_refresh: "刷新", btn_confirm: "✅ 確認取號",
|
header_history: "📋 取號歷史紀錄", btn_refresh_list: "🔄 刷新資料", btn_export: "📊 匯出篩選 Excel",
|
th_filename: "完整檔名", th_status: "狀態", th_unit: "責任單位", th_time: "時間", th_action: "操作",
|
header_backup: "💾 資料庫管理", label_backup: "1. 資料備份", desc_backup: "下載 JSON 備份檔。", btn_download_backup: "⬇️ 下載備份",
|
label_restore: "2. 資料還原", desc_restore: "上傳 JSON (重複者跳過)。", btn_upload_restore: "⬆️ 上傳還原",
|
btn_reset_db: "🗑️ 清空資料庫", btn_clear: "清除", btn_logout: "登出", btn_close: "關閉",
|
btn_export_logs: "📜 匯出 Log",
|
last_backup: "最後備份:",
|
|
label_filter_title: "進階搜尋", label_filter_category: "類別", label_filter_site: "廠別", label_filter_status: "狀態", label_filter_date: "日期區間", label_filter_keyword: "關鍵字",
|
|
prompt_name: "請輸入您的姓名:", user_label: "使用者: ",
|
msg_conn_fail: "⚠️ 無法連線至伺服器",
|
placeholder_custom: "例如: 外殼刮傷", placeholder_search: "關鍵字 (檔名, 說明, 單號...)",
|
alert_success: "✅ 取號成功!", alert_fail: "❌ 失敗: ", alert_id_exist: "🚫 編號已存在!",
|
|
modal_title: "編輯紀錄", label_status: "狀態", label_resp_unit: "責任單位 (可複選)", label_remarks: "備註",
|
btn_cancel: "取消", btn_save_changes: "儲存", btn_edit: "✏️ 編輯", btn_detail: "📄 詳細",
|
status_open: "開立", status_closed: "結案", status_void: "作廢",
|
unit_mgmt: "管理", unit_eng: "工程", unit_pmc: "物管", unit_qa: "品保", unit_pc: "採管", unit_mfg: "製造", unit_pur: "採購", unit_rd: "研發", unit_sales: "業務",
|
unit_ra: "法規", // 新增
|
unit_sup: "供應商",
|
msg_confirm_reset: "⚠️ 危險操作:這將刪除所有資料!\n確定要清空嗎?", msg_reset_ok: "資料庫已清空。", msg_perm_denied: "⛔ 權限不足 (僅限管理員)。",
|
|
header_rules: "NCR 文件命名規範", rule_format_title: "1. 標準格式", rule_format_content: "[類別]-[廠區碼][年月][流水號]-[型號]-[名稱]-[客代]-[日期]",
|
rule_def_title: "2. 欄位定義", th_field: "欄位", th_example: "範例", th_desc: "說明",
|
rule_cat_desc: "NCR: 一般報告; INCR: 內部使用報告", rule_site_desc: "TW: 台北總部; HT: 蘇州合泰廠; VN: 越南廠",
|
rule_month_desc: "西元年月 (6碼)", rule_seq_desc: "該月份該廠區之序號 (固定3碼)", rule_cn_desc: "不可空白。簡述不合格項目或原因。",
|
rule_model_desc: "產品型號 (選填)", rule_cust_desc: "客戶代號 (選填)", rule_date_desc: "YYYYMMDD 非必要項目。若需標註確切日期時加在末段。",
|
|
label_rows_per_page: "每頁筆數:", btn_prev: "上一頁", btn_next: "下一頁",
|
|
label_cust_code: "客戶代號 (選填)", label_recurring: "再發異常", label_model: "型號 (選填)", label_file_date: "日期 (選填)",
|
th_cust_code: "客戶代號", th_recurring: "再發", th_model: "型號",
|
label_filter_recurring_only: "只顯示再發", label_complaint_id: "客訴單號", th_complaint_id: "客訴單號",
|
|
label_penalty_date: "扣款起算日", label_grace_period: "寬限日數", th_penalty_date: "扣款起算", header_due_alert: "⚠️ 即將到期 / 逾期 / 不良率警示",
|
alert_penalty_started: "已逾期 (扣款啟動): {0} 天", label_stop_alert: "不再提醒",
|
label_return_qty: "客退數量", label_shipped_qty: "出貨數量", label_threshold: "不良率警示值 (%)",
|
th_defect_rate: "不良率(%)", alert_defect_rate: "不良率過高 ({0}%)",
|
|
tab_rules: "1. 命名規範", tab_generator: "2. 命名生成器", tab_history: "3. 歷史紀錄查詢", tab_admin: "4. 資料庫管理",
|
role_admin: "管理員", role_editor: "編輯者 (僅修歷史)", role_viewer: "唯讀模式", role_guest: "訪客 (僅限瀏覽規範)",
|
|
msg_gen_readonly: "🔒 唯讀瀏覽模式",
|
msg_gen_editor: "⚠️ 編輯者:僅限修改歷史紀錄",
|
msg_gen_guest: "⛔ 受限存取:僅開放瀏覽規範",
|
label_limit: "上限",
|
|
title_network_links: "網路共用資料夾", link_name_closed: "結案:", link_name_ht: "合泰NCR工作區:", link_name_vn: "越南NCR工作區:", link_name_tw: "台北NCR工作區:",
|
btn_copy: "複製路徑", note_permission: "注意:需向資訊部申請權限才可開啟。請複製路徑後貼上至檔案總管。",
|
msg_copy_ok: "路徑已複製!請貼上至檔案總管。", msg_copy_fail: "複製失敗,請手動選取複製。",
|
|
label_closing_date: "結案日期", th_closing_date: "結案日期",
|
// Admin New Translations
|
btn_admin_tool: "🔧 管理",
|
modal_admin_title: "🔧 管理員專用:核心資料修改",
|
msg_admin_saved: "✅ 資料已更新!檔名已變更。",
|
msg_data_conflict: "⚠️ 資料衝突!\n\n此資料剛被其他使用者修改過。\n系統為保護資料完整性,已阻擋您的存檔。\n\n請重新刷新頁面後再試一次。",
|
// 關聯檔案新翻譯
|
btn_file_link: "關聯檔案",
|
modal_file_title: "📁 關聯檔案設定",
|
label_file_path: "完整路徑與檔案名稱",
|
label_current_link: "目前關聯檔案 (點選可開啟):",
|
btn_clear_path: "清除路徑",
|
msg_file_saved: "✅ 關聯檔案路徑已更新!"
|
},
|
cn: {
|
app_title: "NCR 命名生成器", status_connecting: "连接中...",
|
status_readonly: "🔒 唯读浏览模式",
|
label_category: "文件类别", label_site: "厂区码", label_month: "开单月份 (YYYYMM)", label_seq: "流水号", label_custom_name: "自订名称",
|
opt_ncr: "NCR (一般不合格报告)", opt_incr: "INCR (内部使用)",
|
opt_tw: "TW (台北总部)", opt_ht: "HT (苏州合泰厂)", opt_vn: "VN (越南厂)",
|
msg_fill_data: "请填写资料...", btn_refresh: "刷新", btn_confirm: "✅ 确认取号",
|
header_history: "📋 取号历史记录", btn_refresh_list: "🔄 刷新数据", btn_export: "📊 汇出筛选 Excel",
|
th_filename: "完整文件名", th_status: "状态", th_unit: "责任单位", th_time: "时间", th_action: "操作",
|
header_backup: "💾 数据库管理", label_backup: "1. 资料备份", desc_backup: "下载 JSON 备份档。", btn_download_backup: "⬇️ 下载备份",
|
label_restore: "2. 资料还原", desc_restore: "上传 JSON (重复者跳过)。", btn_upload_restore: "⬆️ 上传还原",
|
btn_reset_db: "🗑️ 清空数据库", btn_clear: "清除", btn_logout: "登出", btn_close: "关闭",
|
btn_export_logs: "📜 汇出 Log",
|
last_backup: "最后备份:",
|
|
label_filter_title: "进阶搜寻", label_filter_category: "类别", label_filter_site: "厂别", label_filter_status: "状态", label_filter_date: "日期区间", label_filter_keyword: "关键字",
|
|
prompt_name: "请输入您的姓名:", user_label: "使用者: ",
|
msg_conn_fail: "⚠️ 无法连线至服务器",
|
placeholder_custom: "例如: 外壳刮伤", placeholder_search: "关键字 (档名, 说明, 单号...)",
|
alert_success: "✅ 取号成功!", alert_fail: "❌ 失败: ", alert_id_exist: "🚫 编号已存在!",
|
modal_title: "编辑记录", label_status: "状态", label_resp_unit: "责任单位 (可复选)", label_remarks: "备注",
|
btn_cancel: "取消", btn_save_changes: "保存", btn_edit: "✏️ 编辑", btn_detail: "📄 详细",
|
status_open: "开立", status_closed: "结案", status_void: "作废",
|
unit_mgmt: "管理", unit_eng: "工程", unit_pmc: "物管", unit_qa: "品保", unit_pc: "采管", unit_mfg: "制造", unit_pur: "采购", unit_rd: "研发", unit_sales: "业务",
|
unit_ra: "法规", // 新增
|
unit_sup: "供应商",
|
msg_confirm_reset: "⚠️ 危险操作:这将删除所有数据!\n确定要清空吗?", msg_reset_ok: "数据库已清空。", msg_perm_denied: "⛔ 权限不足 (仅限管理员)。",
|
|
header_rules: "NCR 文件命名规范", rule_format_title: "1. 标准格式", rule_format_content: "[类别]-[厂区码][年月][流水號]-[型號]-[名称]-[客代]-[日期]",
|
rule_def_title: "2. 栏位定义", th_field: "栏位", th_example: "范例", th_desc: "说明",
|
rule_cat_desc: "NCR: 一般报告; INCR: 内部使用报告", rule_site_desc: "TW: 台北总部; HT: 苏州合泰厂; VN: 越南厂",
|
rule_month_desc: "公元年月 (6码)", rule_seq_desc: "该月份该厂区之序号 (固定3码)", rule_cn_desc: "不可为空。简述不合格项目或原因。",
|
rule_model_desc: "产品型号 (选填)", rule_cust_desc: "客户代号 (选填)", rule_date_desc: "日期后缀 (选填)",
|
|
label_rows_per_page: "每页笔数:", btn_prev: "上一页", btn_next: "下一页",
|
|
label_cust_code: "客户代号 (选填)", label_recurring: "再发异常", label_model: "型号 (选填)", label_file_date: "日期 (选填)",
|
th_cust_code: "客户代号", th_recurring: "再发", th_model: "型号",
|
label_filter_recurring_only: "只显示再发", label_complaint_id: "客诉单号", th_complaint_id: "客诉单号",
|
|
label_penalty_date: "扣款起算日", label_grace_period: "宽限日数", th_penalty_date: "扣款起算", header_due_alert: "⚠️ 即即将到期 / 逾期 / 不良率警示",
|
alert_penalty_started: "已逾期 (扣款启动): {0} 天", label_stop_alert: "不再提醒",
|
label_return_qty: "客退数量", label_shipped_qty: "出货数量", label_threshold: "不良率警示值 (%)",
|
th_defect_rate: "不良率(%)", alert_defect_rate: "不良率过高 ({0}%)",
|
|
tab_rules: "1. 命名规范", tab_generator: "2. 命名生成器", tab_history: "3. 历史记录查询", tab_admin: "4. 数据库管理",
|
role_admin: "管理员", role_editor: "编辑者 (仅修历史)", role_viewer: "唯读模式", role_guest: "访客 (仅限浏览规范)",
|
|
msg_gen_readonly: "🔒 唯读浏览模式",
|
msg_gen_editor: "⚠️ 编辑者:仅限修改历史记录",
|
msg_gen_guest: "⛔ 受限存取:仅开放浏览规范",
|
label_limit: "上限",
|
|
title_network_links: "网络共享资料夹", link_name_closed: "结案:", link_name_ht: "合泰NCR工作区:", link_name_vn: "越南NCR工作区:", link_name_tw: "台北NCR工作区:",
|
btn_copy: "复制路径", note_permission: "注意:需向资讯部申请权限才可开启。请复制路径后贴上至文件资源管理器。",
|
msg_copy_ok: "路径已复制!请粘贴至文件资源管理器。", msg_copy_fail: "复制失败,请手动选取复制。",
|
|
label_closing_date: "结案日期", th_closing_date: "结案日期",
|
// Admin New Translations
|
btn_admin_tool: "🔧 管理",
|
modal_admin_title: "🔧 管理员专用:核心数据修改",
|
msg_admin_saved: "✅ 数据已更新!文件名已变更。",
|
msg_data_conflict: "⚠️ 数据冲突!\n\n此数据刚被其他使用者修改过。\n系统为保护数据完整性,已阻挡您的保存。\n\n请重新刷新页面后再试一次。",
|
// 關聯檔案新翻譯
|
btn_file_link: "关联档案",
|
modal_file_title: "📁 关联档案设置",
|
label_file_path: "完整路径与档案名称",
|
label_current_link: "目前关联档案 (点选可开启):",
|
btn_clear_path: "清除路径",
|
msg_file_saved: "✅ 关联档案路径已更新!"
|
},
|
vi: {
|
app_title: "Hệ thống đặt tên NCR", status_connecting: "Đang kết nối...",
|
status_readonly: "🔒 Chế độ chỉ đọc",
|
label_category: "Loại tệp", label_site: "Nhà máy", label_month: "Tháng (YYYYMM)", label_seq: "Số thứ tự", label_custom_name: "Mô tả",
|
opt_ncr: "NCR (Báo cáo không phù hợp)", opt_incr: "INCR (Nội bộ)",
|
opt_tw: "TW (Đài Bắc)", opt_ht: "HT (Tô Châu)", opt_vn: "VN (Việt Nam)",
|
msg_fill_data: "Vui lòng điền dữ liệu...", btn_refresh: "Làm mới", btn_confirm: "✅ Xác nhận & Lưu",
|
header_history: "📋 Lịch sử lấy số", btn_refresh_list: "🔄 Làm mới dữ liệu", btn_export: "📊 Xuất Excel (Lọc)",
|
th_filename: "Tên tệp", th_status: "Trạng thái", th_unit: "Đơn vị", th_time: "Thời gian", th_action: "Hành động",
|
header_backup: "💾 Quản lý Database", label_backup: "1. Sao lưu", desc_backup: "Tải xuống tệp JSON.", btn_download_backup: "⬇️ Tải xuống",
|
label_restore: "2. Khôi phục", desc_restore: "Tải lên JSON (Gộp).", btn_upload_restore: "⬆️ Tải lên",
|
btn_reset_db: "🗑️ Xóa DB", btn_clear: "Xóa", btn_logout: "Đăng xuất", btn_close: "Đóng",
|
btn_export_logs: "📜 Xuất Logs",
|
last_backup: "Backup cuối: ",
|
|
label_filter_title: "Tìm kiếm nâng cao", label_filter_category: "Loại tệp", label_filter_site: "Nhà máy", label_filter_status: "Trạng thái", label_filter_date: "Ngày tháng", label_filter_keyword: "Từ khóa",
|
|
prompt_name: "Nhập tên của bạn:", user_label: "Người dùng: ",
|
msg_conn_fail: "⚠️ Lỗi kết nối máy chủ",
|
placeholder_custom: "Ví dụ: Trầy xước vỏ", placeholder_search: "Từ khóa (Tên tệp, Mô tả...)",
|
alert_success: "✅ Đã lưu thành công!", alert_fail: "❌ Thất bại: ", alert_id_exist: "🚫 ID đã tồn tại!",
|
modal_title: "Chỉnh sửa", label_status: "Trạng thái", label_resp_unit: "Đơn vị chịu trách nhiệm", label_remarks: "Ghi chú",
|
btn_cancel: "Hủy", btn_save_changes: "Lưu", btn_edit: "✏️ Sửa", btn_detail: "📄 Chi tiết",
|
status_open: "Mở", status_closed: "Đóng", status_void: "Vô hiệu",
|
unit_mgmt: "Quản lý", unit_eng: "Kỹ thuật", unit_pmc: "QL Vật liệu", unit_qa: "QA", unit_pc: "KS Mua", unit_mfg: "Sản xuất", unit_pur: "Mua hàng", unit_rd: "R&D", unit_sales: "Kinh doanh",
|
unit_ra: "Pháp quy", // 新增
|
unit_sup: "NCC",
|
msg_confirm_reset: "⚠️ NGUY HIỂM: Dữ liệu sẽ bị xóa!\nBạn có chắc không?", msg_reset_ok: "Đã xóa dữ liệu.", msg_perm_denied: "⛔ Không có quyền (Chỉ Admin).",
|
|
header_rules: "Quy tắc đặt tên", rule_format_title: "1. Định dạng", rule_format_content: "[Loại]-[NM][NămTháng][STT]-[MôHình]-[MôTả]-[MãKH]-[Ngày]",
|
rule_def_title: "2. Định nghĩa", th_field: "Trường", th_example: "Ví dụ", th_desc: "Mô tả",
|
rule_cat_desc: "INCR: Nội bộ", rule_site_desc: "TW/HT/VN",
|
rule_month_desc: "Tháng phát hành", rule_seq_desc: "Duy nhất", rule_cn_desc: "Mô tả ngắn gọn",
|
rule_model_desc: "Mô hình (Tùy chọn)", rule_cust_desc: "Mã KH (Tùy chọn)", rule_date_desc: "Ngày (Tùy chọn)",
|
|
label_rows_per_page: "Số dòng/trang:", btn_prev: "Trước", btn_next: "Sau",
|
|
label_cust_code: "Mã KH", label_recurring: "Lặp lại", label_model: "Mô hình", label_file_date: "Ngày",
|
th_cust_code: "Mã KH", th_recurring: "Lặp", th_model: "Mô hình",
|
label_filter_recurring_only: "Chỉ lặp lại", label_complaint_id: "Mã khiếu nại", th_complaint_id: "Mã khiếu nại",
|
|
label_penalty_date: "Ngày phạt", label_grace_period: "Gia hạn (Ngày)", th_penalty_date: "Ngày phạt", header_due_alert: "⚠️ Cảnh báo quá hạn",
|
alert_penalty_started: "Đã quá hạn: {0} ngày", label_stop_alert: "Không nhắc lại",
|
label_return_qty: "SL Trả", label_shipped_qty: "SL Xuất", label_threshold: "Ngưỡng lỗi (%)",
|
th_defect_rate: "Tỷ lệ lỗi(%)", alert_defect_rate: "Tỷ lệ lỗi cao ({0}%)",
|
|
tab_rules: "1. Quy tắc", tab_generator: "2. Máy tạo tên", tab_history: "3. Lịch sử", tab_admin: "4. Quản trị",
|
role_admin: "Admin", role_editor: "Biên tập", role_viewer: "Chỉ xem", role_guest: "Khách (Chỉ xem quy tắc)",
|
|
msg_gen_readonly: "🔒 Chế độ chỉ đọc",
|
msg_gen_editor: "⚠️ Biên tập viên: Chỉ sửa lịch sử",
|
msg_gen_guest: "⛔ Quyền hạn chế: Chỉ xem quy tắc",
|
label_limit: "Giới hạn",
|
|
title_network_links: "Thư mục mạng", link_name_closed: "Đã đóng:", link_name_ht: "Khu vực HT:", link_name_vn: "Khu vực VN:", link_name_tw: "Khu vực TW:",
|
btn_copy: "Sao chép", note_permission: "Lưu ý: Cần quyền IT. Vui lòng sao chép đường dẫn và dán vào File Explorer.",
|
msg_copy_ok: "Đã sao chép đường dẫn! Vui lòng dán vào File Explorer.", msg_copy_fail: "Sao chép thất bại. Vui lòng sao chép thủ công.",
|
|
label_closing_date: "Ngày đóng", th_closing_date: "Ngày đóng",
|
// Admin New Translations
|
btn_admin_tool: "🔧 Quản trị",
|
modal_admin_title: "🔧 Chỉnh sửa dữ liệu quản trị",
|
msg_admin_saved: "✅ Đã cập nhật! Tên tệp đã đổi.",
|
msg_data_conflict: "⚠️ XUNG ĐỘT DỮ LIỆU!\n\nDữ liệu này vừa được sửa bởi người khác.\nHệ thống đã chặn lưu để bảo vệ dữ liệu.\n\nVui lòng làm mới trang và thử lại.",
|
// 關聯檔案新翻譯
|
btn_file_link: "Liên kết file",
|
modal_file_title: "📁 Thiết lập file liên kết",
|
label_file_path: "Đường dẫn đầy đủ và tên file",
|
label_current_link: "File liên kết hiện tại (Bấm để mở):",
|
btn_clear_path: "Xóa đường dẫn",
|
msg_file_saved: "✅ Đường dẫn file đã được cập nhật!"
|
}
|
};
|
|
let curLang = 'en';
|
let currentUser = localStorage.getItem('ncr_user');
|
let historyData = [];
|
let currentSort = { field: 'timestamp', dir: 'desc' }; // Keep sort by timestamp internally initially if desired, or change to closingDate
|
let lastBackupTime = "";
|
|
let currentPage = 1;
|
let itemsPerPage = 20;
|
let currentFilteredData = [];
|
let currentRole = 'VIEWER';
|
|
function t(key, arg1) {
|
let str = translations[curLang][key] || translations['en'][key] || key;
|
if(arg1 !== undefined) str = str.replace('{0}', arg1);
|
return str;
|
}
|
|
function detectLanguage() {
|
const storedLang = localStorage.getItem('ncr_lang');
|
if (storedLang) return storedLang;
|
const navLang = (navigator.language || navigator.userLanguage).toLowerCase();
|
if (navLang.includes('vi')) return 'vi';
|
if (navLang.includes('zh')) return (navLang.includes('cn') || navLang.includes('sg')) ? 'cn' : 'tw';
|
return 'en';
|
}
|
|
function changeLanguage(lang) {
|
curLang = lang;
|
localStorage.setItem('ncr_lang', lang);
|
document.getElementById('langSwitcher').value = lang;
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
const key = el.getAttribute('data-i18n');
|
if (key) {
|
if(el.tagName === 'INPUT') {
|
if (key === 'label_model') document.getElementById('searchModel').placeholder = t(key);
|
else if (key === 'label_cust_code') document.getElementById('searchCustCode').placeholder = t(key);
|
else el.placeholder = t(key);
|
}
|
else el.innerText = t(key);
|
}
|
});
|
|
document.getElementById('customName').placeholder = t('placeholder_custom');
|
document.getElementById('searchInput').placeholder = t('placeholder_search');
|
document.getElementById('searchModel').placeholder = t('label_model');
|
document.getElementById('searchCustCode').placeholder = t('label_cust_code');
|
|
updateUserDisplay();
|
updateBackupTimeDisplay();
|
|
const optStatus = document.getElementById('editStatus').options;
|
optStatus[0].text = t('status_open'); optStatus[1].text = t('status_closed'); optStatus[2].text = t('status_void');
|
const updateOpt = (id, map) => {
|
const sel = document.getElementById(id);
|
for(let i=0; i<sel.options.length; i++) {
|
const key = map[sel.options[i].value];
|
if(key) sel.options[i].text = t(key);
|
}
|
};
|
updateOpt('category', {'NCR':'opt_ncr', 'INCR':'opt_incr'});
|
updateOpt('site', {'TW':'opt_tw', 'HT':'opt_ht', 'VN':'opt_vn'});
|
|
renderUnitCheckboxes();
|
applyUIPermissions(); // Refresh generator status text
|
applyFilterAndSort();
|
}
|
|
async function fetchLastBackup() {
|
try {
|
const res = await fetch('/api/last_backup');
|
const data = await res.json();
|
lastBackupTime = data.time || "None";
|
updateBackupTimeDisplay();
|
} catch(e) { console.error("Failed to fetch backup time"); }
|
}
|
|
function updateBackupTimeDisplay() {
|
const el = document.getElementById('backupTimeDisplay');
|
if(el) el.innerText = t('last_backup') + lastBackupTime;
|
}
|
|
function updateUserDisplay() {
|
let roleKey = 'role_viewer';
|
if (currentRole === 'ADMIN') roleKey = 'role_admin';
|
else if (currentRole === 'EDITOR') roleKey = 'role_editor';
|
else if (currentRole === 'GUEST') roleKey = 'role_guest';
|
|
document.getElementById('currentUserDisplay').innerText = t('user_label') + currentUser + " [" + t(roleKey) + "]";
|
}
|
|
function openTab(tabId) {
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
document.getElementById(tabId).classList.add('active');
|
document.querySelector(`.tab-btn[onclick="openTab('${tabId}')"]`).classList.add('active');
|
localStorage.setItem('ncr_active_tab', tabId);
|
}
|
|
// Toggle collapsible section
|
function toggleSection(elementId) {
|
const container = document.getElementById(elementId);
|
container.classList.toggle('is-collapsed');
|
}
|
|
// --- RBAC Core Logic ---
|
async function checkPermission() {
|
try {
|
const res = await fetch('/api/my_role');
|
if (res.ok) {
|
const data = await res.json();
|
currentRole = data.role;
|
console.log(`Permission check: ${currentRole} (IP: ${data.ip})`);
|
} else {
|
currentRole = 'GUEST';
|
}
|
} catch(e) {
|
currentRole = 'GUEST';
|
}
|
applyUIPermissions();
|
}
|
|
function applyUIPermissions() {
|
const isViewer = (currentRole === 'VIEWER');
|
const isAdmin = (currentRole === 'ADMIN');
|
const isEditor = (currentRole === 'EDITOR');
|
const isGuest = (currentRole === 'GUEST');
|
|
// Banner Color
|
const banner = document.querySelector('.user-banner');
|
if (isAdmin) banner.style.backgroundColor = '#3b82f6';
|
else if (isGuest) banner.style.backgroundColor = '#334155';
|
else banner.style.backgroundColor = 'var(--readonly-banner)';
|
|
updateUserDisplay();
|
|
const tabGen = document.getElementById('tabBtnGen');
|
const tabHist = document.getElementById('tabBtnHist');
|
const tabAdmin = document.getElementById('tabBtnAdmin');
|
|
if (isAdmin) {
|
tabGen.classList.remove('tab-hidden');
|
tabHist.classList.remove('tab-hidden');
|
tabAdmin.classList.remove('tab-hidden');
|
} else if (isEditor || isViewer) {
|
tabGen.classList.add('tab-hidden');
|
tabHist.classList.remove('tab-hidden');
|
tabAdmin.classList.add('tab-hidden');
|
} else {
|
tabGen.classList.add('tab-hidden');
|
tabHist.classList.add('tab-hidden');
|
tabAdmin.classList.add('tab-hidden');
|
}
|
|
const activeTab = document.querySelector('.tab-content.active').id;
|
if ((isGuest && activeTab !== 'tab-rules') ||
|
((isEditor || isViewer) && (activeTab === 'tab-generator' || activeTab === 'tab-admin'))) {
|
openTab('tab-rules');
|
}
|
|
const canGenerate = isAdmin;
|
const genInputs = document.querySelectorAll('#generatorForm input, #generatorForm select, #generatorForm textarea');
|
genInputs.forEach(el => el.disabled = !canGenerate);
|
const btnSave = document.getElementById('btnSave');
|
btnSave.style.display = canGenerate ? 'inline-block' : 'none';
|
btnSave.disabled = !canGenerate;
|
|
const finalNameDiv = document.getElementById('finalName');
|
if (!isAdmin) {
|
if (isGuest) finalNameDiv.innerText = t('msg_gen_guest');
|
else finalNameDiv.innerText = isEditor ? t('msg_gen_editor') : t('msg_gen_readonly');
|
|
finalNameDiv.style.color = isEditor ? "#b45309" : "#64748b";
|
}
|
|
const btnRestore = document.getElementById('btnRestore');
|
const btnReset = document.getElementById('btnResetDB');
|
const adminRows = document.querySelectorAll('.admin-only-row');
|
|
if (isAdmin) {
|
if(btnRestore) { btnRestore.disabled = false; btnRestore.parentElement.style.display = 'flex'; }
|
if(btnReset) btnReset.style.display = 'inline-block';
|
adminRows.forEach(el => el.style.display = 'flex');
|
} else {
|
if(btnRestore) { btnRestore.disabled = true; btnRestore.parentElement.style.display = 'none'; }
|
if(btnReset) btnReset.style.display = 'none';
|
adminRows.forEach(el => el.style.display = 'none');
|
}
|
|
if (!isGuest && historyData.length > 0) renderPagination();
|
if (isGuest) {
|
document.getElementById('historyBody').innerHTML = '';
|
document.getElementById('pageInfoDisplay').innerText = 'Restricted';
|
}
|
}
|
|
function copyToClipboard(text) {
|
if (navigator.clipboard && window.isSecureContext) {
|
navigator.clipboard.writeText(text).then(() => {
|
alert(t('msg_copy_ok'));
|
}, (err) => {
|
fallbackCopyText(text);
|
});
|
} else {
|
fallbackCopyText(text);
|
}
|
}
|
|
function fallbackCopyText(text) {
|
const textArea = document.createElement("textarea");
|
textArea.value = text;
|
textArea.style.position = "fixed";
|
document.body.appendChild(textArea);
|
textArea.focus();
|
textArea.select();
|
try {
|
document.execCommand('copy');
|
alert(t('msg_copy_ok'));
|
} catch (err) {
|
console.error('Fallback: Oops, unable to copy', err);
|
alert(t('msg_copy_fail'));
|
}
|
document.body.removeChild(textArea);
|
}
|
|
window.onload = async function() {
|
curLang = detectLanguage();
|
changeLanguage(curLang);
|
|
if (!currentUser) {
|
let inputName = prompt(t('prompt_name'));
|
|
if (!inputName || inputName.trim() === "") {
|
let msg = "Error: Name is required!";
|
if (curLang === 'tw') msg = "錯誤:請輸入姓名!";
|
else if (curLang === 'cn') msg = "错误:请输入姓名!";
|
else if (curLang === 'vi') msg = "Lỗi: Vui lòng nhập tên!";
|
|
alert(msg);
|
location.reload();
|
return;
|
}
|
|
currentUser = inputName.trim();
|
localStorage.setItem('ncr_user', currentUser);
|
}
|
|
document.getElementById('yearMonth').value = new Date().toISOString().slice(0, 7);
|
|
const savedSite = localStorage.getItem('ncr_site');
|
const savedCat = localStorage.getItem('ncr_cat');
|
if(savedSite) document.getElementById('site').value = savedSite;
|
if(savedCat) document.getElementById('category').value = savedCat;
|
|
const savedRows = localStorage.getItem('ncr_rows_per_page');
|
if(savedRows) {
|
itemsPerPage = parseInt(savedRows);
|
document.getElementById('rowsPerPage').value = savedRows;
|
}
|
|
const lastTab = localStorage.getItem('ncr_active_tab');
|
if(lastTab && document.getElementById(lastTab)) {
|
openTab(lastTab);
|
}
|
|
await checkPermission();
|
|
if (currentRole !== 'GUEST') {
|
loadHistory();
|
}
|
|
renderUnitCheckboxes();
|
logLogin();
|
fetchLastBackup();
|
};
|
|
async function logLogin() {
|
try {
|
await fetch('/api/log_login', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify({ user: currentUser })
|
});
|
} catch(e) { console.log('Login log failed'); }
|
}
|
|
function downloadLogs() { window.location.href = '/api/export_logs'; }
|
|
async function logout() {
|
try {
|
await fetch('/api/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user: currentUser }) });
|
} catch(e) {}
|
localStorage.removeItem('ncr_user');
|
window.location.href = '/login.html';
|
}
|
|
async function loadHistory() {
|
try {
|
const res = await fetch('/api/history');
|
historyData = await res.json();
|
applyFilterAndSort();
|
updateSequenceDisplayOnly();
|
} catch(e) { console.error(e); }
|
}
|
|
function clearSearch() {
|
document.querySelectorAll('.checkbox-options input[type="checkbox"]').forEach(cb => cb.checked = false);
|
document.getElementById('searchStart').value = '';
|
document.getElementById('searchEnd').value = '';
|
document.getElementById('searchInput').value = '';
|
document.getElementById('searchModel').value = '';
|
document.getElementById('searchCustCode').value = '';
|
document.getElementById('filterRecurring').checked = false;
|
applyFilterAndSort();
|
}
|
|
function applyFilterAndSort() {
|
const categories = Array.from(document.querySelectorAll('input[name="filterCategory"]:checked')).map(cb => cb.value);
|
const sites = Array.from(document.querySelectorAll('input[name="filterSite"]:checked')).map(cb => cb.value);
|
const statuses = Array.from(document.querySelectorAll('input[name="filterStatus"]:checked')).map(cb => cb.value);
|
const startDate = document.getElementById('searchStart').value;
|
const endDate = document.getElementById('searchEnd').value;
|
const keyword = document.getElementById('searchInput').value.toLowerCase();
|
const filterRecurring = document.getElementById('filterRecurring').checked;
|
const searchModel = document.getElementById('searchModel').value.toLowerCase();
|
const searchCustCode = document.getElementById('searchCustCode').value.toLowerCase();
|
|
currentFilteredData = historyData.filter(item => {
|
let rQty = parseInt(item.returnQty) || 0;
|
let sQty = parseInt(item.shippedQty) || 0;
|
item.calculatedRate = (sQty > 0) ? (rQty / sQty * 100) : 0;
|
|
if (categories.length > 0 && !categories.includes(item.category)) return false;
|
if (sites.length > 0 && !sites.includes(item.site)) return false;
|
let currStatus = item.status || 'Open';
|
if (statuses.length > 0 && !statuses.includes(currStatus)) return false;
|
let itemDate = item.timestamp.substring(0, 10);
|
if (startDate && itemDate < startDate) return false;
|
if (endDate && itemDate > endDate) return false;
|
|
if (filterRecurring && item.recurring !== 'Y') return false;
|
|
if (searchModel && !String(item.model || '').toLowerCase().includes(searchModel)) return false;
|
if (searchCustCode && !String(item.customerCode || '').toLowerCase().includes(searchCustCode)) return false;
|
let unitStr = "";
|
let unitTransStr = "";
|
if (Array.isArray(item.responsibleUnit)) {
|
// 1. 原始代碼字串
|
unitStr = item.responsibleUnit.join(" ");
|
|
// 2. 翻譯後的名稱字串
|
item.responsibleUnit.forEach(u => {
|
if (u.startsWith('SUP:')) {
|
unitTransStr += u.split(':', 2)[1] + " ";
|
} else {
|
const uInfo = RESP_UNITS.find(x => x.code === u);
|
if (uInfo) {
|
unitTransStr += t(uInfo.key) + " ";
|
}
|
}
|
});
|
}
|
|
// 組合所有可搜尋的文字內容 (全部轉小寫)
|
const content = (
|
String(item.fullFileName) +
|
String(item.remarks || "") +
|
String(item.customName || "") +
|
unitStr + // 代碼 (ENG)
|
unitTransStr + // 翻譯名稱 (工程)
|
String(item.customerCode || "") +
|
String(item.complaintId || "")
|
).toLowerCase();
|
|
|
if (keyword && !content.includes(keyword)) return false;
|
return true;
|
});
|
|
currentFilteredData.sort((a, b) => {
|
let valA = a[currentSort.field];
|
let valB = b[currentSort.field];
|
if (currentSort.field === 'calculatedRate' || currentSort.field === 'gracePeriod') {
|
valA = parseFloat(valA) || 0;
|
valB = parseFloat(valB) || 0;
|
} else {
|
if (Array.isArray(valA)) valA = valA.join(',');
|
if (Array.isArray(valB)) valB = valB.join(',');
|
valA = String(valA || "").toLowerCase();
|
valB = String(valB || "").toLowerCase();
|
}
|
if (valA < valB) return currentSort.dir === 'asc' ? -1 : 1;
|
if (valA > valB) return currentSort.dir === 'asc' ? 1 : -1;
|
return 0;
|
});
|
|
document.querySelectorAll('.sort-icon').forEach(el => el.innerText = '');
|
const activeIcon = document.getElementById(`sort_${currentSort.field}`);
|
if(activeIcon) activeIcon.innerText = currentSort.dir === 'asc' ? '▲' : '▼';
|
|
currentPage = 1;
|
renderPagination();
|
updateDueAlerts(historyData);
|
}
|
|
function updateDueAlerts(data) {
|
const container = document.getElementById('dueAlertContent');
|
const section = document.getElementById('dueAlertSection');
|
container.innerHTML = '';
|
let hasAlerts = false;
|
const today = new Date();
|
today.setHours(0,0,0,0);
|
|
data.forEach(row => {
|
if(row.status !== 'Open') return;
|
if(row.stopAlert === 'Y') return;
|
|
if(row.penaltyDate) {
|
const pDate = new Date(row.penaltyDate);
|
const grace = parseInt(row.gracePeriod) || 0;
|
const triggerDate = new Date(pDate);
|
triggerDate.setDate(triggerDate.getDate() + grace);
|
|
if(today > triggerDate) {
|
hasAlerts = true;
|
const diffTime = today - triggerDate;
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
const clickAttr = (currentRole !== 'VIEWER' && currentRole !== 'GUEST') ? `onclick="openEditModal('${row.uniqueId}')"` : '';
|
const cursorStyle = (currentRole !== 'VIEWER' && currentRole !== 'GUEST') ? 'cursor:pointer; text-decoration:underline;' : '';
|
|
const div = document.createElement('div');
|
div.className = 'due-item';
|
div.innerHTML = `
|
<span class="due-badge">${t('alert_penalty_started', diffDays)}</span>
|
<strong style="${cursorStyle}" ${clickAttr}>${row.fullFileName}</strong>
|
<span>(${row.penaltyDate} + ${grace}d)</span>
|
`;
|
container.appendChild(div);
|
}
|
}
|
// Defect Rate check
|
let rQty = parseInt(row.returnQty) || 0;
|
let sQty = parseInt(row.shippedQty) || 0;
|
let rate = (sQty > 0) ? (rQty / sQty * 100) : 0;
|
let threshold = parseFloat(row.defectThreshold) || 0;
|
|
if (threshold > 0 && rate > threshold) {
|
hasAlerts = true;
|
const clickAttr = (currentRole !== 'VIEWER' && currentRole !== 'GUEST') ? `onclick="openEditModal('${row.uniqueId}')"` : '';
|
const cursorStyle = (currentRole !== 'VIEWER' && currentRole !== 'GUEST') ? 'cursor:pointer; text-decoration:underline;' : '';
|
const div = document.createElement('div');
|
div.className = 'due-item';
|
div.innerHTML = `
|
<span class="due-badge" style="background:#b91c1c;">${t('alert_defect_rate', rate.toFixed(2))}</span>
|
<strong style="${cursorStyle}" ${clickAttr}>${row.fullFileName}</strong>
|
<span>(${t('label_limit')}: ${threshold}%)</span>
|
`;
|
container.appendChild(div);
|
}
|
});
|
section.style.display = hasAlerts ? 'block' : 'none';
|
}
|
|
function sortBy(field) {
|
if (currentSort.field === field) currentSort.dir = currentSort.dir === 'asc' ? 'desc' : 'asc';
|
else { currentSort.field = field; currentSort.dir = 'desc'; }
|
applyFilterAndSort();
|
}
|
|
function changeRowsPerPage() {
|
itemsPerPage = parseInt(document.getElementById('rowsPerPage').value);
|
localStorage.setItem('ncr_rows_per_page', itemsPerPage);
|
currentPage = 1;
|
renderPagination();
|
}
|
|
function prevPage() {
|
if(currentPage > 1) { currentPage--; renderPagination(); }
|
}
|
|
function nextPage() {
|
const maxPage = Math.ceil(currentFilteredData.length / itemsPerPage);
|
if(currentPage < maxPage) { currentPage++; renderPagination(); }
|
}
|
|
function renderPagination() {
|
const totalItems = currentFilteredData.length;
|
if(totalItems === 0) {
|
document.getElementById('pageInfoDisplay').innerText = `Showing 0-0 of 0`;
|
renderHistory([]);
|
document.getElementById('btnPrevPage').disabled = true;
|
document.getElementById('btnNextPage').disabled = true;
|
return;
|
}
|
|
const maxPage = Math.ceil(totalItems / itemsPerPage);
|
if(currentPage > maxPage) currentPage = maxPage;
|
|
const startIdx = (currentPage - 1) * itemsPerPage;
|
const endIdx = Math.min(startIdx + itemsPerPage, totalItems);
|
|
document.getElementById('pageInfoDisplay').innerText = `Showing ${startIdx + 1}-${endIdx} of ${totalItems}`;
|
document.getElementById('btnPrevPage').disabled = (currentPage === 1);
|
document.getElementById('btnNextPage').disabled = (currentPage === maxPage);
|
|
const pageData = currentFilteredData.slice(startIdx, endIdx);
|
renderHistory(pageData);
|
}
|
|
function renderHistory(data) {
|
const tbody = document.getElementById('historyBody');
|
tbody.innerHTML = '';
|
data.forEach(row => {
|
const tr = document.createElement('tr');
|
let status = row.status || 'Open';
|
let statusClass = status === 'Closed' ? 'status-closed' : (status === 'Void' ? 'status-void' : 'status-open');
|
let statusText = t('status_' + status.toLowerCase()) || status;
|
|
let unitHtml = "";
|
let units = Array.isArray(row.responsibleUnit) ? row.responsibleUnit : (row.responsibleUnit ? [row.responsibleUnit] : []);
|
units.forEach(u => {
|
if (u.startsWith('SUP:')) {
|
const supName = u.split(':', 2)[1];
|
unitHtml += `<span class="unit-badge unit-badge-sup" title="${supName}">${t('unit_sup')}: ${supName}</span>`;
|
} else {
|
const uInfo = RESP_UNITS.find(x => x.code === u);
|
unitHtml += `<span class="unit-badge">${uInfo ? t(uInfo.key) : u}</span>`;
|
}
|
});
|
|
const closingDateDisplay = row.closingDate || '-';
|
|
const remarkHtml = row.remarks ? `<div style="font-size:0.8rem; color:#64748b; font-style:italic; white-space: pre-wrap;">📝 ${row.remarks}</div>` : '';
|
const recurHtml = (row.recurring === 'Y') ? '<span class="recurring-badge">R</span>' : '';
|
const penaltyHtml = row.penaltyDate ? `<div style="font-size:0.8rem; color:#e11d48;">${row.penaltyDate} (+${row.gracePeriod || 0}d)</div>` : '';
|
|
let rateDisplay = (row.calculatedRate !== undefined) ? row.calculatedRate.toFixed(2) + '%' : '0.00%';
|
if (row.calculatedRate > (parseFloat(row.defectThreshold) || 9999)) {
|
rateDisplay = `<span style="color:red; font-weight:bold;">${rateDisplay}</span>`;
|
}
|
|
let displayFileName = row.fullFileName;
|
const dashes = displayFileName.split('-');
|
if(dashes.length >= 3) {
|
const prefix = dashes.slice(0, 2).join('-');
|
const id = dashes[2];
|
const suffix = dashes.slice(3).join('-');
|
displayFileName = `${prefix}-<b>${id}</b>${suffix ? '-' + suffix : ''}`;
|
}
|
|
// 修正:同時處理冒號與反斜線
|
if (row.filePath) {
|
let safeLink = row.filePath.replace(/:/g, '%3A').replace(/\\/g, '/'); // 冒號以 %3A 替換,反斜線轉正斜線
|
if (!safeLink.startsWith('william-open://')) {
|
safeLink = 'william-open://' + safeLink;
|
}
|
displayFileName += `<br><a href="${safeLink}" style="color: #0ea5e9; font-size: 0.82rem; text-decoration: underline; font-weight: normal; margin-top: 3px; display: inline-block;">📁 ${row.filePath}</a>`;
|
}
|
|
// 修改 Action 區塊邏輯
|
const actionCellStyle = "display: flex; gap: 5px; align-items: center; justify-content: flex-start;";
|
// 預設隱藏
|
let btnContainerStyle = 'display:none;';
|
|
// 只有 ADMIN 或 EDITOR 可以看到按鈕容器
|
if (currentRole === 'ADMIN' || currentRole === 'EDITOR') {
|
btnContainerStyle = actionCellStyle;
|
}
|
|
// 定義按鈕 HTML
|
let buttonsHtml = '';
|
|
// 1. 編輯按鈕 (一般權限)
|
buttonsHtml += `<button class="btn-edit" onclick="openEditModal('${row.uniqueId}')">${t('btn_edit')}</button>`;
|
|
// 關聯檔案按鈕
|
buttonsHtml += `<button class="btn-file-link" style="background-color: #0ea5e9; padding: 5px 10px; font-size: 0.85rem; border: none; border-radius: 6px; color: white; cursor: pointer; white-space: nowrap;" onclick="openFileModal('${row.uniqueId}')">${t('btn_file_link')}</button>`;
|
|
// 2. 新增:Admin 專用按鈕 (排在編輯後面)
|
if (currentRole === 'ADMIN') {
|
buttonsHtml += `<button class="btn-admin-tool" onclick="openAdminModal('${row.uniqueId}')">${t('btn_admin_tool')}</button>`;
|
}
|
|
tr.innerHTML = `
|
<td style="font-family:monospace; font-weight:bold;">${displayFileName} ${remarkHtml}</td>
|
<td style="text-align:center;"><span class="${statusClass}">${statusText}</span></td>
|
<td>${unitHtml}</td>
|
<td>${row.model || ''}</td>
|
<td>${row.customerCode || ''}</td>
|
<td>${row.complaintId || ''}</td>
|
<td style="text-align:right;">${rateDisplay}</td>
|
<td>${penaltyHtml}</td>
|
<td style="text-align:center;">${recurHtml}</td>
|
<td style="font-size:0.9rem; text-align:center;">${closingDateDisplay}</td>
|
<td>
|
<div style="${btnContainerStyle}">
|
${buttonsHtml}
|
</div>
|
</td>
|
`;
|
tbody.appendChild(tr);
|
});
|
}
|
|
function updateSequenceDisplayOnly() {
|
const cat = document.getElementById('category').value;
|
const site = document.getElementById('site').value;
|
localStorage.setItem('ncr_site', site);
|
localStorage.setItem('ncr_cat', cat);
|
|
const ym = document.getElementById('yearMonth').value.replace('-', '');
|
let maxSeq = 0;
|
historyData.forEach(r => {
|
if (r.category === cat && r.site === site && r.yearMonth === ym) {
|
const s = parseInt(r.sequence);
|
if (!isNaN(s) && s > maxSeq) maxSeq = s;
|
}
|
});
|
document.getElementById('sequence').value = maxSeq + 1;
|
previewName();
|
}
|
|
function previewName() {
|
if (currentRole !== 'ADMIN') return;
|
|
const cat = document.getElementById('category').value;
|
const site = document.getElementById('site').value;
|
const ym = document.getElementById('yearMonth').value.replace('-', '');
|
const seq = String(document.getElementById('sequence').value).padStart(3, '0');
|
const custom = document.getElementById('customName').value.trim();
|
const model = document.getElementById('modelName').value.trim();
|
const custCode = document.getElementById('customerCode').value.trim();
|
const fileDate = document.getElementById('fileDate').value.trim();
|
|
const parts = [ `${cat}-${site}${ym}${seq}`, model, custom, custCode, fileDate ];
|
const name = parts.filter(Boolean).join('-');
|
|
if (!custom) {
|
document.getElementById('finalName').innerText = t('msg_fill_data');
|
document.getElementById('btnSave').disabled = true;
|
} else {
|
document.getElementById('finalName').innerText = name + " (ID generated by server)";
|
document.getElementById('btnSave').disabled = false;
|
}
|
}
|
|
async function saveData() {
|
if (currentRole !== 'ADMIN') return;
|
|
const record = {
|
category: document.getElementById('category').value,
|
site: document.getElementById('site').value,
|
yearMonth: document.getElementById('yearMonth').value,
|
customName: document.getElementById('customName').value,
|
customerCode: document.getElementById('customerCode').value.trim(),
|
model: document.getElementById('modelName').value.trim(),
|
fileDate: document.getElementById('fileDate').value.trim(),
|
recurring: document.getElementById('recurring').checked ? 'Y' : 'N'
|
};
|
|
try {
|
document.getElementById('btnSave').disabled = true;
|
document.getElementById('btnSave').innerText = "Saving...";
|
|
const res = await fetch('/api/save', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(record)
|
});
|
|
if (res.status === 409) { alert("System busy (ID conflict), please try again."); return; }
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
|
const result = await res.json();
|
if (result.success) {
|
const msgDiv = document.getElementById('saveSuccessMsg');
|
msgDiv.style.display = 'block';
|
msgDiv.innerHTML = `✅ ${t('alert_success')}<br><div style="font-size:1.2rem; font-weight:bold; color:#000; margin-top:5px; user-select:all; font-family:monospace;">${result.savedFileName}</div>`;
|
alert(t('alert_success'));
|
|
document.getElementById('customName').value = '';
|
document.getElementById('modelName').value = '';
|
document.getElementById('customerCode').value = '';
|
document.getElementById('fileDate').value = '';
|
document.getElementById('recurring').checked = false;
|
loadHistory();
|
} else {
|
alert(t('alert_fail') + result.msg);
|
}
|
} catch(e) { alert(t('msg_conn_fail')); }
|
finally {
|
document.getElementById('btnSave').disabled = false;
|
document.getElementById('btnSave').innerText = t('btn_confirm');
|
}
|
}
|
|
function downloadBackup() {
|
window.location.href = '/api/backup';
|
setTimeout(fetchLastBackup, 2000);
|
}
|
|
async function uploadRestore() {
|
const fileInput = document.getElementById('restoreFile');
|
if(fileInput.files.length === 0) { alert(t('msg_select_file')); return; }
|
if(!confirm(t('msg_confirm_restore'))) return;
|
const formData = new FormData(); formData.append('file', fileInput.files[0]);
|
try {
|
const res = await fetch('/api/restore', { method: 'POST', body: formData });
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
const result = await res.json();
|
if(result.msg === 'RESTORE_OK') { alert(`Restore OK! Success: ${result.s}, Skipped: ${result.k}`); loadHistory(); } else alert(result.msg);
|
} catch(e) { alert(t('msg_restore_fail')); }
|
}
|
|
function exportExcel() {
|
const categories = Array.from(document.querySelectorAll('input[name="filterCategory"]:checked')).map(cb => cb.value).join(',');
|
const sites = Array.from(document.querySelectorAll('input[name="filterSite"]:checked')).map(cb => cb.value).join(',');
|
const statuses = Array.from(document.querySelectorAll('input[name="filterStatus"]:checked')).map(cb => cb.value).join(',');
|
const start = document.getElementById('searchStart').value;
|
const end = document.getElementById('searchEnd').value;
|
const keyword = document.getElementById('searchInput').value;
|
const model = document.getElementById('searchModel').value;
|
const cust = document.getElementById('searchCustCode').value;
|
|
const params = new URLSearchParams({
|
lang: curLang,
|
categories: categories,
|
sites: sites,
|
status: statuses,
|
start: start,
|
end: end,
|
search: keyword,
|
model: model,
|
cust: cust
|
});
|
window.location.href = `/api/export_excel?${params.toString()}`;
|
}
|
|
async function resetDatabase() {
|
if(!confirm(t('msg_confirm_reset'))) return;
|
try {
|
const res = await fetch('/api/reset', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({user: currentUser}) });
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
const result = await res.json();
|
if(result.success) { alert(t('msg_reset_ok')); loadHistory(); }
|
else alert(t('alert_fail') + result.msg);
|
} catch(e) { alert(t('msg_conn_fail')); }
|
}
|
|
function renderUnitCheckboxes() {
|
const container = document.getElementById('unitCheckboxGroup'); container.innerHTML = '';
|
RESP_UNITS.forEach(unit => {
|
const label = document.createElement('label'); label.className = 'checkbox-label';
|
label.innerHTML = `<input type="checkbox" value="${unit.code}"><span>${t(unit.key)}</span>`;
|
container.appendChild(label);
|
});
|
}
|
|
function toggleEditSupInput() {
|
const chk = document.getElementById('editChkSup');
|
const txt = document.getElementById('editTxtSup');
|
if(chk.checked) {
|
txt.style.display = 'block';
|
txt.focus();
|
} else {
|
txt.style.display = 'none';
|
txt.value = '';
|
}
|
}
|
|
function toggleClosingDateInput() {
|
const status = document.getElementById('editStatus').value;
|
const dateInput = document.getElementById('editClosingDate');
|
|
if (status === 'Closed') {
|
dateInput.disabled = false;
|
if (!dateInput.value) {
|
dateInput.value = new Date().toISOString().split('T')[0];
|
}
|
} else {
|
dateInput.disabled = true;
|
dateInput.value = '';
|
}
|
}
|
|
function openEditModal(uniqueId) {
|
if (currentRole === 'VIEWER' || currentRole === 'GUEST') return;
|
|
const record = historyData.find(r => r.uniqueId === uniqueId);
|
if(!record) return;
|
document.getElementById('editUniqueId').value = record.uniqueId;
|
// 樂觀鎖:紀錄當前版本
|
document.getElementById('editVersion').value = record.version || 0;
|
|
document.getElementById('editStatus').value = record.status || 'Open';
|
document.getElementById('editRemarks').value = record.remarks || '';
|
|
document.getElementById('editComplaintId').value = record.complaintId || '';
|
document.getElementById('editRecurring').checked = (record.recurring === 'Y');
|
|
document.getElementById('editPenaltyDate').value = record.penaltyDate || '';
|
document.getElementById('editGracePeriod').value = record.gracePeriod || 0;
|
document.getElementById('editStopAlert').checked = (record.stopAlert === 'Y');
|
|
document.getElementById('editReturnQty').value = record.returnQty || 0;
|
document.getElementById('editShippedQty').value = record.shippedQty || 0;
|
document.getElementById('editDefectThreshold').value = record.defectThreshold || 0;
|
|
document.getElementById('editClosingDate').value = record.closingDate || '';
|
|
toggleClosingDateInput();
|
|
const units = record.responsibleUnit || [];
|
document.querySelectorAll('#unitCheckboxGroup input[type="checkbox"]').forEach(cb => cb.checked = units.includes(cb.value));
|
|
const supUnit = units.find(u => u.startsWith('SUP:'));
|
const chkSup = document.getElementById('editChkSup');
|
const txtSup = document.getElementById('editTxtSup');
|
|
if (supUnit) {
|
chkSup.checked = true;
|
txtSup.value = supUnit.split(':', 2)[1];
|
txtSup.style.display = 'block';
|
} else {
|
chkSup.checked = false;
|
txtSup.value = '';
|
txtSup.style.display = 'none';
|
}
|
|
document.getElementById('editModal').style.display = 'block';
|
}
|
|
function closeEditModal() { document.getElementById('editModal').style.display = 'none'; }
|
|
async function saveEditModal() {
|
if (currentRole === 'VIEWER' || currentRole === 'GUEST') return;
|
|
const uniqueId = document.getElementById('editUniqueId').value;
|
const version = parseInt(document.getElementById('editVersion').value) || 0;
|
const status = document.getElementById('editStatus').value;
|
const remarks = document.getElementById('editRemarks').value;
|
const complaintId = document.getElementById('editComplaintId').value.trim();
|
const recurring = document.getElementById('editRecurring').checked ? 'Y' : 'N';
|
const penaltyDate = document.getElementById('editPenaltyDate').value;
|
const gracePeriod = parseInt(document.getElementById('editGracePeriod').value) || 0;
|
const stopAlert = document.getElementById('editStopAlert').checked ? 'Y' : 'N';
|
const returnQty = parseInt(document.getElementById('editReturnQty').value) || 0;
|
const shippedQty = parseInt(document.getElementById('editShippedQty').value) || 0;
|
const defectThreshold = parseFloat(document.getElementById('editDefectThreshold').value) || 0;
|
const closingDate = document.getElementById('editClosingDate').value;
|
|
const units = [];
|
document.querySelectorAll('#unitCheckboxGroup input[type="checkbox"]:checked').forEach(cb => units.push(cb.value));
|
|
const chkSup = document.getElementById('editChkSup');
|
const txtSup = document.getElementById('editTxtSup');
|
if (chkSup.checked && txtSup.value.trim()) {
|
units.push(`SUP:${txtSup.value.trim()}`);
|
} else if (chkSup.checked && !txtSup.value.trim()) {
|
alert('Please enter supplier name.');
|
return;
|
}
|
|
try {
|
const payload = {
|
uniqueId, version, status, remarks, responsibleUnit: units,
|
complaintId, recurring,
|
penaltyDate, gracePeriod, stopAlert,
|
returnQty, shippedQty, defectThreshold,
|
closingDate
|
};
|
|
const res = await fetch('/api/update', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(payload)
|
});
|
|
if (res.status === 409) {
|
alert(t('msg_data_conflict'));
|
closeEditModal();
|
loadHistory();
|
return;
|
}
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
|
const result = await res.json();
|
if(result.success) { closeEditModal(); loadHistory(); } else alert(t('alert_fail') + result.msg);
|
} catch(e) { alert(t('msg_conn_fail')); }
|
}
|
|
// --- Admin Tool Functions ---
|
|
function openAdminModal(uniqueId) {
|
if (currentRole !== 'ADMIN') return;
|
|
const record = historyData.find(r => r.uniqueId === uniqueId);
|
if (!record) return;
|
|
document.getElementById('adminUniqueId').value = record.uniqueId;
|
// 樂觀鎖:紀錄當前版本
|
document.getElementById('adminVersion').value = record.version || 0;
|
document.getElementById('adminDisplayId').value = record.uniqueId;
|
|
document.getElementById('adminCustomName').value = record.customName || '';
|
document.getElementById('adminModel').value = record.model || '';
|
document.getElementById('adminCustCode').value = record.customerCode || '';
|
document.getElementById('adminFileDate').value = record.fileDate || '';
|
|
['adminDisplayId', 'adminCustomName', 'adminModel', 'adminCustCode', 'adminFileDate'].forEach(id => {
|
document.getElementById(id).oninput = previewAdminFilename;
|
});
|
|
previewAdminFilename();
|
document.getElementById('adminModal').style.display = 'block';
|
}
|
|
function closeAdminModal() {
|
document.getElementById('adminModal').style.display = 'none';
|
}
|
|
function previewAdminFilename() {
|
const uid = document.getElementById('adminDisplayId').value;
|
const model = document.getElementById('adminModel').value.trim();
|
const name = document.getElementById('adminCustomName').value.trim();
|
const code = document.getElementById('adminCustCode').value.trim();
|
const date = document.getElementById('adminFileDate').value.trim();
|
|
const parts = [uid, model, name, code, date];
|
const fullname = parts.filter(p => p).join('-');
|
|
document.getElementById('adminPreviewName').innerText = fullname;
|
}
|
|
async function saveAdminModal() {
|
if (currentRole !== 'ADMIN') return;
|
|
const payload = {
|
originalUniqueId: document.getElementById('adminUniqueId').value, // 隱藏欄位:舊ID
|
newUniqueId: document.getElementById('adminDisplayId').value.trim(), // 輸入框:新ID
|
version: parseInt(document.getElementById('adminVersion').value) || 0,
|
customName: document.getElementById('adminCustomName').value.trim(),
|
model: document.getElementById('adminModel').value.trim(),
|
customerCode: document.getElementById('adminCustCode').value.trim(),
|
fileDate: document.getElementById('adminFileDate').value.trim()
|
};
|
|
if (!payload.customName || !payload.newUniqueId) {
|
alert("ID and Description (Custom Name) are required!");
|
return;
|
}
|
|
try {
|
const res = await fetch('/api/admin_update', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(payload)
|
});
|
|
if (res.status === 409) {
|
const result = await res.json();
|
if (result.msg === 'NEW_ID_ALREADY_EXISTS') {
|
alert(t('alert_id_exist'));
|
} else {
|
alert(t('msg_data_conflict'));
|
closeAdminModal();
|
loadHistory();
|
}
|
return;
|
}
|
|
const result = await res.json();
|
if (result.success) {
|
alert(t('msg_admin_saved'));
|
closeAdminModal();
|
loadHistory();
|
} else {
|
alert(t('alert_fail') + result.msg);
|
}
|
} catch (e) {
|
alert(t('msg_conn_fail'));
|
}
|
}
|
|
// --- 關聯檔案設定 Functions ---
|
|
function openFileModal(uniqueId) {
|
if (currentRole === 'VIEWER' || currentRole === 'GUEST') return;
|
|
const record = historyData.find(r => r.uniqueId === uniqueId);
|
if(!record) return;
|
document.getElementById('fileUniqueId').value = record.uniqueId;
|
document.getElementById('fileVersion').value = record.version || 0;
|
|
const currentPath = record.filePath || '';
|
document.getElementById('fileInputPath').value = currentPath;
|
|
const previewContainer = document.getElementById('filePreviewContainer');
|
const previewLink = document.getElementById('filePreviewLink');
|
|
if (currentPath) {
|
// 修正:同時處理冒號與反斜線
|
let safeLink = currentPath.replace(/:/g, '%3A').replace(/\\/g, '/');
|
if (!safeLink.startsWith('william-open://')) {
|
safeLink = 'william-open://' + safeLink;
|
}
|
previewLink.innerHTML = `<a href="${safeLink}" style="color: #0ea5e9; font-weight: bold; text-decoration: underline;">📁 ${currentPath}</a>`;
|
previewContainer.style.display = 'block';
|
} else {
|
previewContainer.style.display = 'none';
|
}
|
|
document.getElementById('fileModal').style.display = 'block';
|
}
|
|
function closeFileModal() {
|
document.getElementById('fileModal').style.display = 'none';
|
}
|
|
async function clearFilePath() {
|
if (currentRole === 'VIEWER' || currentRole === 'GUEST') return;
|
|
const uniqueId = document.getElementById('fileUniqueId').value;
|
const version = parseInt(document.getElementById('fileVersion').value) || 0;
|
const filePath = ""; // 設定為空字串以進行清除
|
|
try {
|
const res = await fetch('/api/update_filepath', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify({ uniqueId, version, filePath })
|
});
|
|
if (res.status === 409) {
|
alert(t('msg_data_conflict'));
|
closeFileModal();
|
loadHistory();
|
return;
|
}
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
|
const result = await res.json();
|
if(result.success) {
|
// 1. 重新整理背景的歷史紀錄列表
|
loadHistory();
|
|
// 2. 保持視窗開啟,但即時清空目前視窗內的 UI 輸入框與預覽連結
|
document.getElementById('fileInputPath').value = '';
|
document.getElementById('filePreviewContainer').style.display = 'none';
|
|
// 3. 重要防呆:將隱藏欄位的版本號自動 +1,確保不關閉視窗下接著輸入新路徑儲存時不會發生衝突
|
document.getElementById('fileVersion').value = version + 1;
|
} else {
|
alert(t('alert_fail') + result.msg);
|
}
|
} catch(e) { alert(t('msg_conn_fail')); }
|
}
|
|
async function saveFileModal() {
|
if (currentRole === 'VIEWER' || currentRole === 'GUEST') return;
|
|
const uniqueId = document.getElementById('fileUniqueId').value;
|
const version = parseInt(document.getElementById('fileVersion').value) || 0;
|
// const filePath = document.getElementById('fileInputPath').value.trim();
|
let filePath = document.getElementById('fileInputPath').value.trim();
|
filePath = filePath.replace(/"/g, '');
|
try {
|
const res = await fetch('/api/update_filepath', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify({ uniqueId, version, filePath })
|
});
|
|
if (res.status === 409) {
|
alert(t('msg_data_conflict'));
|
closeFileModal();
|
loadHistory();
|
return;
|
}
|
if (res.status === 403) { alert(t('msg_perm_denied')); return; }
|
|
const result = await res.json();
|
if(result.success) {
|
// alert(t('msg_file_saved'));
|
closeFileModal();
|
loadHistory();
|
} else {
|
alert(t('alert_fail') + result.msg);
|
}
|
} catch(e) { alert(t('msg_conn_fail')); }
|
}
|
</script>
|
|
</body>
|
</html>
|