<!DOCTYPE html>
|
<html lang="en">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>NCR Detail View</title>
|
<style>
|
:root { --primary-color: #2563eb; --bg-color: #f8fafc; --border-color: #cbd5e1; --readonly-bg: #f1f5f9; }
|
body { font-family: "Microsoft JhengHei", "Segoe UI", Roboto, sans-serif; background: var(--bg-color); margin: 0; padding: 20px; display: flex; justify-content: center; color: #334155; }
|
|
/* Layout */
|
.container { background: white; width: 100%; max-width: 900px; padding: 30px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; position: relative; }
|
|
/* Header */
|
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 2px solid #e2e8f0; padding-bottom: 15px; margin-bottom: 20px; }
|
h2 { margin: 0 0 5px 0; color: #1e293b; display: flex; align-items: center; gap: 10px; }
|
.id-tag { background: #eff6ff; color: #1d4ed8; padding: 4px 8px; border-radius: 6px; font-family: monospace; font-size: 1rem; border: 1px solid #bfdbfe; }
|
|
/* Form Grid */
|
.section-title { font-weight: bold; color: var(--primary-color); margin-top: 25px; margin-bottom: 15px; border-left: 4px solid var(--primary-color); padding-left: 10px; font-size: 1.1rem; background: #f0f9ff; padding: 5px 10px; border-radius: 0 4px 4px 0; }
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
.full-width { grid-column: span 2; }
|
|
/* Special layout for 4 items in a row */
|
.four-col-row { grid-column: span 2; display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
|
/* Inputs */
|
.form-group { margin-bottom: 5px; }
|
label { display: block; margin-bottom: 6px; font-weight: bold; font-size: 0.9rem; color: #475569; }
|
input[type="text"], select, textarea { width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 6px; box-sizing: border-box; font-size: 0.95rem; transition: 0.2s; }
|
input:focus, textarea:focus, select:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
|
textarea { resize: vertical; min-height: 80px; line-height: 1.5; }
|
|
/* Read Only State */
|
input:disabled, select:disabled, textarea:disabled { background-color: var(--readonly-bg); color: #64748b; cursor: not-allowed; border-color: #e2e8f0; }
|
.readonly-badge { display: none; background: #64748b; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; font-weight: bold; }
|
|
/* Buttons */
|
.btn-bar { margin-top: 30px; padding-top: 20px; border-top: 1px solid #e2e8f0; display: flex; justify-content: flex-end; gap: 10px; position: sticky; bottom: 0; background: white; padding-bottom: 10px; }
|
button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; color: white; transition: 0.2s; display: flex; align-items: center; gap: 5px; font-size: 0.95rem; }
|
button:active { transform: translateY(1px); }
|
.btn-save { background-color: #10b981; box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2); }
|
.btn-save:hover { background-color: #059669; }
|
.btn-close { background-color: #64748b; }
|
.btn-close:hover { background-color: #475569; }
|
|
.lang-select { padding: 4px 8px; border-radius: 6px; border: 1px solid #cbd5e1; font-size: 0.85rem; background: white; cursor: pointer; }
|
|
@media (max-width: 768px) {
|
.form-grid { grid-template-columns: 1fr; }
|
.four-col-row { grid-template-columns: 1fr 1fr; } /* Mobile: 2x2 */
|
.full-width { grid-column: span 1; }
|
.four-col-row { grid-column: span 1; }
|
}
|
</style>
|
</head>
|
<body>
|
|
<div class="container">
|
<div class="header">
|
<div>
|
<h2>
|
<span data-i18n="title_detail">Detailed Maintenance</span>
|
<span id="readonlyBadge" class="readonly-badge" data-i18n="lbl_readonly">READ-ONLY</span>
|
</h2>
|
<div style="margin-top:8px;">
|
<span id="recordIdDisplay" class="id-tag">Loading...</span>
|
</div>
|
</div>
|
<div>
|
<select class="lang-select" id="langSwitcher" onchange="changeLanguage(this.value)">
|
<option value="en">English</option>
|
<option value="tw">繁體中文</option>
|
<option value="cn">简体中文</option>
|
<option value="vi">Tiếng Việt</option>
|
</select>
|
</div>
|
</div>
|
|
<form id="detailForm">
|
<input type="hidden" id="uniqueId">
|
|
<div class="section-title" data-i18n="sec_basic">Basic Info</div>
|
<div class="form-grid">
|
<div class="form-group full-width">
|
<label data-i18n="lbl_exception_desc">Exception Description</label>
|
<textarea id="exceptionDesc"></textarea>
|
</div>
|
|
<div class="four-col-row">
|
<div class="form-group">
|
<label data-i18n="lbl_severity">MA/MI</label>
|
<select id="severity">
|
<option value="">-- Select --</option>
|
<option value="MA">Major (MA)</option>
|
<option value="MI">Minor (MI)</option>
|
</select>
|
</div>
|
<div class="form-group">
|
<label data-i18n="lbl_type">Type</label>
|
<input type="text" id="issueType">
|
</div>
|
<div class="form-group">
|
<label data-i18n="lbl_source">Source</label>
|
<input type="text" id="source">
|
</div>
|
<div class="form-group">
|
<label data-i18n="lbl_issuer">Issuer</label>
|
<input type="text" id="issuer">
|
</div>
|
</div>
|
|
<div class="form-group full-width">
|
<label data-i18n="lbl_handling_desc">Non-conforming Product Handling</label>
|
<textarea id="handlingDesc"></textarea>
|
</div>
|
</div>
|
|
<div class="section-title" data-i18n="sec_resp">Responsible Unit Info</div>
|
<div class="form-grid">
|
<div class="form-group">
|
<label data-i18n="lbl_resp_type">Supplier / Dept</label>
|
<select id="respUnitType">
|
<option value="Dept" data-i18n="opt_dept">Department</option>
|
<option value="Supplier" data-i18n="opt_sup">Supplier</option>
|
</select>
|
</div>
|
<div class="form-group">
|
<label data-i18n="lbl_resp_name">Unit Name</label>
|
<input type="text" id="respUnitName" data-i18n="ph_resp_name" placeholder="e.g. Mold Dept / ABC Co.">
|
</div>
|
<div class="form-group full-width">
|
<label data-i18n="lbl_resp_person">Responsible Person</label>
|
<input type="text" id="respPerson">
|
</div>
|
</div>
|
|
<div class="section-title" data-i18n="sec_analysis">Analysis & Action</div>
|
<div class="form-grid">
|
<div class="form-group full-width">
|
<label data-i18n="lbl_root_cause">Root Cause Analysis</label>
|
<textarea id="rootCause" placeholder="..."></textarea>
|
</div>
|
<div class="form-group full-width">
|
<label data-i18n="lbl_action">Corrective Action</label>
|
<textarea id="correctiveAction" placeholder="..."></textarea>
|
</div>
|
<div class="form-group full-width">
|
<label data-i18n="lbl_tracking">Response Date / Tracking Result</label>
|
<textarea id="trackingResult" data-i18n="ph_tracking" placeholder="e.g. 1st Date: 2025/01/01 - Result... 2nd Date: 2025/01/10 - Result..." style="min-height: 100px; background-color:#f0fdf4; border-color:#86efac;"></textarea>
|
</div>
|
</div>
|
|
<div class="btn-bar">
|
<button type="button" class="btn-close" onclick="window.close()" data-i18n="btn_close">Close</button>
|
<button type="button" class="btn-save" id="btnSave" onclick="saveData()" data-i18n="btn_save">💾 Save Changes</button>
|
</div>
|
</form>
|
</div>
|
|
<script>
|
const translations = {
|
en: {
|
title_detail: "Detailed Maintenance", lbl_readonly: "🔒 READ-ONLY",
|
sec_basic: "Basic Information",
|
lbl_type: "Type", lbl_source: "Source", lbl_severity: "MA/MI", lbl_issuer: "Issuer",
|
lbl_exception_desc: "Exception Description", lbl_handling_desc: "Product Handling Description",
|
sec_resp: "Responsible Unit Information",
|
lbl_resp_type: "Unit Type", lbl_resp_name: "Unit Name (Dept/Sup)", lbl_resp_person: "Person in Charge",
|
ph_resp_name: "e.g. Mold Dept / ABC Co.",
|
opt_dept: "Department", opt_sup: "Supplier",
|
sec_analysis: "Analysis & Countermeasure",
|
lbl_root_cause: "Root Cause Analysis", lbl_action: "Improvement Countermeasure",
|
lbl_tracking: "Response Date / Tracking Result",
|
ph_tracking: "e.g. \n1st Date: 2025/01/01 - Result... \n2nd Date: 2025/01/10 - Result...",
|
btn_close: "Close", btn_save: "💾 Save Changes",
|
msg_saved: "✅ Saved successfully!", msg_err: "Error: ", msg_perm_denied: "⛔ Permission Denied (Admin/Editor Only)"
|
},
|
tw: {
|
title_detail: "詳細內容維護", lbl_readonly: "🔒 唯讀模式",
|
sec_basic: "異常基本資訊",
|
lbl_type: "類型", lbl_source: "來源", lbl_severity: "MA/MI (嚴重度)", lbl_issuer: "開單負責人",
|
lbl_exception_desc: "異常狀況說明", lbl_handling_desc: "不合格品處理說明",
|
sec_resp: "責任單位資訊",
|
lbl_resp_type: "單位類別", lbl_resp_name: "責任單位名稱 (部門/供應商)", lbl_resp_person: "責任單位負責人",
|
ph_resp_name: "例如:模具部 / ABC 公司",
|
opt_dept: "部門", opt_sup: "供應商",
|
sec_analysis: "原因分析與對策",
|
lbl_root_cause: "責任單位原因分析", lbl_action: "責任單位改善對策",
|
lbl_tracking: "回覆日期 / 追蹤結果",
|
ph_tracking: "例如:\n第一次:2025/01/01 - 結果...\n第二次:2025/01/10 - 結果...",
|
btn_close: "關閉視窗", btn_save: "💾 儲存變更",
|
msg_saved: "✅ 儲存成功!", msg_err: "錯誤: ", msg_perm_denied: "⛔ 權限不足 (僅限管理員/編輯者)"
|
},
|
cn: {
|
title_detail: "详细内容维护", lbl_readonly: "🔒 唯读模式",
|
sec_basic: "异常基本信息",
|
lbl_type: "类型", lbl_source: "来源", lbl_severity: "MA/MI (严重度)", lbl_issuer: "开单负责人",
|
lbl_exception_desc: "异常状况说明", lbl_handling_desc: "不合格品处理说明",
|
sec_resp: "责任单位信息",
|
lbl_resp_type: "单位类别", lbl_resp_name: "责任单位名称 (部门/供应商)", lbl_resp_person: "责任单位负责人",
|
ph_resp_name: "例如:模具部 / ABC 公司",
|
opt_dept: "部门", opt_sup: "供应商",
|
sec_analysis: "原因分析与对策",
|
lbl_root_cause: "责任单位原因分析", lbl_action: "责任单位改善对策",
|
lbl_tracking: "回复日期 / 追踪结果",
|
ph_tracking: "例如:\n第一次:2025/01/01 - 结果...\n第二次:2025/01/10 - 结果...",
|
btn_close: "关闭窗口", btn_save: "💾 保存变更",
|
msg_saved: "✅ 保存成功!", msg_err: "错误: ", msg_perm_denied: "⛔ 权限不足 (仅限管理员/编辑者)"
|
},
|
vi: {
|
title_detail: "Bảo trì chi tiết", lbl_readonly: "🔒 CHỈ ĐỌC",
|
sec_basic: "Thông tin cơ bản",
|
lbl_type: "Loại", lbl_source: "Nguồn", lbl_severity: "MA/MI", lbl_issuer: "Người phát hành",
|
lbl_exception_desc: "Mô tả ngoại lệ", lbl_handling_desc: "Xử lý sản phẩm lỗi",
|
sec_resp: "Đơn vị chịu trách nhiệm",
|
lbl_resp_type: "Loại đơn vị", lbl_resp_name: "Tên đơn vị (Bộ phận/NCC)", lbl_resp_person: "Người chịu trách nhiệm",
|
ph_resp_name: "Ví dụ: Phòng Khuôn / Công ty ABC",
|
opt_dept: "Bộ phận", opt_sup: "Nhà cung cấp",
|
sec_analysis: "Phân tích & Đối sách",
|
lbl_root_cause: "Phân tích nguyên nhân gốc rễ", lbl_action: "Đối sách cải tiến",
|
lbl_tracking: "Ngày phản hồi / Kết quả theo dõi",
|
ph_tracking: "Ví dụ: \nLần 1: 2025/01/01 - Kết quả...\nLần 2: 2025/01/10 - Kết quả...",
|
btn_close: "Đóng", btn_save: "💾 Lưu thay đổi",
|
msg_saved: "✅ Đã lưu thành công!", msg_err: "Lỗi: ", msg_perm_denied: "⛔ Từ chối quyền (Chỉ Admin/Editor)"
|
}
|
};
|
|
let originalData = {};
|
let curLang = localStorage.getItem('ncr_lang') || 'en';
|
let canEdit = false;
|
|
function changeLanguage(lang) {
|
curLang = lang;
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
const key = el.getAttribute('data-i18n');
|
if (translations[lang][key]) {
|
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') el.placeholder = translations[lang][key];
|
else el.innerText = translations[lang][key];
|
}
|
});
|
|
const optDept = document.querySelector('option[value="Dept"]');
|
const optSup = document.querySelector('option[value="Supplier"]');
|
if(translations[lang]['opt_dept']) optDept.text = translations[lang]['opt_dept'];
|
if(translations[lang]['opt_sup']) optSup.text = translations[lang]['opt_sup'];
|
}
|
|
const urlParams = new URLSearchParams(window.location.search);
|
const uniqueId = urlParams.get('id');
|
|
window.onload = async function() {
|
document.getElementById('langSwitcher').value = curLang;
|
changeLanguage(curLang);
|
|
if (!uniqueId) {
|
alert('Error: No ID provided!');
|
window.close();
|
return;
|
}
|
document.getElementById('uniqueId').value = uniqueId;
|
document.getElementById('recordIdDisplay').innerText = uniqueId;
|
|
await checkPermission();
|
loadData();
|
};
|
|
async function checkPermission() {
|
try {
|
const res = await fetch('/api/my_role');
|
const data = await res.json();
|
const role = data.role;
|
|
if (role === 'ADMIN' || role === 'EDITOR') {
|
canEdit = true;
|
} else {
|
canEdit = false;
|
disableForm();
|
}
|
console.log(`User Role: ${role}, Can Edit: ${canEdit}`);
|
|
} catch (e) {
|
console.error("Permission check failed", e);
|
canEdit = false;
|
disableForm();
|
}
|
}
|
|
function disableForm() {
|
const inputs = document.querySelectorAll('input, select, textarea');
|
inputs.forEach(el => el.disabled = true);
|
const btnSave = document.getElementById('btnSave');
|
if(btnSave) btnSave.style.display = 'none';
|
const badge = document.getElementById('readonlyBadge');
|
if(badge) badge.style.display = 'inline-block';
|
}
|
|
async function loadData() {
|
try {
|
const res = await fetch(`/api/record/${uniqueId}`);
|
if (!res.ok) throw new Error('Failed to load data');
|
originalData = await res.json();
|
populateForm(originalData);
|
} catch (e) {
|
alert("Cannot load data: " + e.message);
|
}
|
}
|
|
function populateForm(data) {
|
const fields = [
|
'issueType', 'source', 'severity', 'issuer', 'exceptionDesc', 'handlingDesc',
|
'respUnitType', 'respUnitName', 'respPerson', 'rootCause', 'correctiveAction', 'trackingResult'
|
];
|
|
fields.forEach(f => {
|
if (document.getElementById(f)) {
|
document.getElementById(f).value = data[f] || '';
|
}
|
});
|
}
|
|
async function saveData() {
|
if (!canEdit) {
|
alert(translations[curLang]['msg_perm_denied']);
|
return;
|
}
|
|
const updateData = { ...originalData };
|
const fields = [
|
'issueType', 'source', 'severity', 'issuer', 'exceptionDesc', 'handlingDesc',
|
'respUnitType', 'respUnitName', 'respPerson', 'rootCause', 'correctiveAction', 'trackingResult'
|
];
|
|
fields.forEach(f => {
|
updateData[f] = document.getElementById(f).value;
|
});
|
|
const btnSave = document.getElementById('btnSave');
|
const originalText = btnSave.innerText;
|
btnSave.disabled = true;
|
btnSave.innerText = "Saving...";
|
|
try {
|
const res = await fetch('/api/update', {
|
method: 'POST',
|
headers: { 'Content-Type': 'application/json' },
|
body: JSON.stringify(updateData)
|
});
|
|
if (res.status === 403) {
|
alert(translations[curLang]['msg_perm_denied']);
|
return;
|
}
|
|
const result = await res.json();
|
if (result.success) {
|
alert(translations[curLang]['msg_saved']);
|
} else {
|
alert(translations[curLang]['msg_err'] + result.msg);
|
}
|
} catch (e) {
|
alert(translations[curLang]['msg_err'] + e.message);
|
} finally {
|
btnSave.disabled = false;
|
btnSave.innerText = originalText;
|
}
|
}
|
</script>
|
|
</body>
|
</html>
|