<!DOCTYPE html>
<html lang="en">
<head>
<base target="_top">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/rowgroup/1.4.0/css/rowGroup.dataTables.min.css">
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
<script src="https://cdn.datatables.net/rowgroup/1.4.0/js/dataTables.rowGroup.min.js"></script>
<script src="https://unpkg.com/html5-qrcode"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
:root { --gdt-bg: #f4f7f9; --gdt-card-bg: #ffffff; --gdt-primary: #4f46e5; --gdt-primary-hover: #4338ca; --gdt-text-main: #1e293b; --gdt-text-muted: #64748b; --gdt-border: #e2e8f0; }
body { background-color: var(--gdt-bg); font-family: 'Inter', sans-serif; color: var(--gdt-text-main); min-height: 100vh; overflow-x: hidden; }
.card, .modal-content { background-color: var(--gdt-card-bg) !important; border: 1px solid rgba(0,0,0,0.05) !important; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03); }
.modal-header { background-color: #f8fafc; border-bottom: 1px solid var(--gdt-border); border-top-left-radius: 16px; border-top-right-radius: 16px; color: var(--gdt-text-main); }
.form-control, .form-select { border-radius: 8px; border: 1px solid #cbd5e1; font-size: 14px; padding: 10px 14px; transition: all 0.15s ease-in-out; }
.form-control:focus, .form-select:focus { border-color: var(--gdt-primary); box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); outline: none; }
.form-control:disabled { background-color: #f1f5f9; }
.btn { border-radius: 8px; font-weight: 500; font-size: 14px; }
.btn-primary { background-color: var(--gdt-primary); border: none; }
label.fw-bold { font-size: 0.75rem; color: var(--gdt-text-muted); margin-bottom: 6px; display: block; font-weight: 600; }
.accordion-button { font-weight: 600; background-color: #f8fafc; border-radius: 8px !important; margin-bottom: 8px; border: 1px solid var(--gdt-border); font-size: 14px; }
.accordion-button:not(.collapsed) { background-color: #eff6ff; color: var(--gdt-primary); box-shadow: none; border-bottom: none; }
.accordion-body { background: #fff; border: 1px solid var(--gdt-border); border-top: none; margin-bottom: 8px; padding: 16px; border-radius: 0 0 8px 8px; }
.top-badge { background-color: #e0e7ff; color: var(--gdt-primary); font-size: 0.65rem; font-weight: 700; padding: 4px 8px; border-radius: 4px; }
.category-pill { background-color: #ffffff; border: 1px solid #e2e8f0; color: #4f46e5; font-weight: 600; padding: 8px 16px; border-radius: 50px; font-size: 13.5px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.02); display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.category-pill:hover { border-color: #818cf8; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }
.category-pill.active { background-color: #4f46e5; color: #ffffff; border-color: #4f46e5; }
.category-pill .badge-count { background-color: #e0e7ff; color: #4f46e5; font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 700; }
.category-pill.active .badge-count { background-color: #ffffff; color: #4f46e5; }
.category-pill.warning-pill { border-color: #fca5a5; color: #dc2626; }
.category-pill.warning-pill.active { background-color: #dc2626; color: #ffffff; border-color: #dc2626; }
.category-pill.warning-pill .badge-count { background-color: #fee2e2; color: #dc2626; }
.category-pill.warning-pill.active .badge-count { background-color: #ffffff; color: #dc2626; }
.asset-card { background-color: #ffffff; border: 1px solid #bfdbfe; border-radius: 12px; padding: 16px; margin-bottom: 12px; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.03); }
.asset-card:hover { border-color: #60a5fa; box-shadow: 0 4px 12px rgba(0,0,0,0.06); transform: translateY(-1px); }
.asset-card.old-pc-alert { border: 2px solid #fb923c; background-color: #fffaf0; }
.custom-badge { font-size: 11px; font-weight: 600; padding: 5px 14px; border-radius: 20px; display: inline-block; }
.warning-badge { background-color: #fee2e2; color: #dc2626; font-size: 11px; font-weight: 700; padding: 5px 10px; border-radius: 20px; margin-right: 8px; }
.search-container { position: relative; width: 100%; margin-bottom: 20px; }
.search-container i { position: absolute; left: 14px; top: 12px; color: #94a3b8; }
.search-container input { padding-left: 40px; border-radius: 12px; border: 1px solid #cbd5e1; width: 100%; padding-top: 12px; padding-bottom: 12px; font-size: 15px; }
table.dataTable thead { display: none; }
table.dataTable.no-footer { border-bottom: none !important; }
table.dataTable tbody td { padding: 4px 0 !important; border-bottom: none !important; }
.group-header { cursor:pointer; background-color: #f8fafc !important; font-weight: 600 !important; padding: 12px !important; border-radius: 8px; border: 1px solid #e2e8f0; margin-bottom: 8px; display: block; }
.badge-count-folder { background-color: #1e293b !important; color: #ffffff !important; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 700; margin-left: 8px; }
#historyContainer::-webkit-scrollbar { width: 6px; }
#historyContainer::-webkit-scrollbar-thumb { background-color: #cbd5e1; border-radius: 4px; }
.timeline-change-box { background-color: #f8fafc; border: 1px dashed #cbd5e1; border-radius: 6px; padding: 10px; font-size: 12.5px; line-height: 1.5; color: #334155; }
</style>
</head>
<body>
<!-- LOGIN VIEW -->
<div id="loginView" style="height: 100vh; display: flex; align-items: center; justify-content: center; padding: 15px;">
<div class="card p-4 mx-auto w-100" style="max-width: 400px;">
<div class="text-center mb-4"><h4 class="fw-bold mb-1">IT Asset Portal</h4><span class="top-badge">SECURE LOGIN</span></div>
<form id="loginForm" onsubmit="event.preventDefault(); attemptLogin();">
<div class="mb-3"><label class="fw-bold">Officer ID</label><input type="text" id="officerId" class="form-control" autocomplete="username"></div>
<div class="mb-3"><label class="fw-bold">PIN Code</label><input type="password" id="pinCode" class="form-control" autocomplete="current-password"></div>
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="form-check"><input class="form-check-input" type="checkbox" id="rememberMe"><label class="form-check-label text-muted small" for="rememberMe">Remember me</label></div>
<a href="#" onclick="forgotPinModal.show(); return false;" class="small fw-bold text-primary text-decoration-none">Forgot PIN?</a>
</div>
<button type="submit" class="btn btn-primary w-100" id="loginBtn">Authenticate</button>
<div id="loginMessage" class="mt-3 text-center fw-bold small"></div>
</form>
</div>
</div>
<!-- RECOVER PIN MODAL -->
<div class="modal fade" id="forgotPinModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header p-3"><h6 class="modal-title fw-bold m-0 text-primary">Recover PIN</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body p-4 bg-light">
<label class="fw-bold">Officer ID</label><input type="text" id="fp_officerId" class="form-control mb-2">
<div id="fpMessage" class="text-center small fw-bold"></div>
</div>
<div class="modal-footer p-3 bg-white"><button class="btn btn-primary w-100 fw-bold" id="fpSendBtn" onclick="submitForgotPin()">Send PIN to Email</button></div>
</div>
</div>
</div>
<!-- DASHBOARD VIEW -->
<div id="dashboardView" style="display: none; padding: 20px 10px;">
<div class="d-flex justify-content-between align-items-center mb-4 mx-auto" style="max-width: 1200px;">
<div>
<div class="d-flex align-items-center gap-2 mb-1"><h4 class="fw-bold m-0" id="welcomeName"></h4><span class="top-badge">LIVE MATRIX</span></div>
<span class="text-muted small" id="assignedBranch"></span>
</div>
<div class="d-flex gap-2">
<button class="btn btn-light btn-sm text-primary fw-bold shadow-sm" onclick="openPinModal()">Change PIN</button>
<button class="btn btn-light btn-sm text-danger fw-bold shadow-sm" onclick="logout()">Logout</button>
</div>
</div>
<div class="card mx-auto mb-4" style="max-width: 1200px; background: transparent; box-shadow: none; border: none !important;">
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="search-container mb-0" style="max-width: 400px; flex-grow: 1;">
<i class="bi bi-search"></i>
<input type="text" id="globalSearch" placeholder="Search by Hostname, Barcode, User, Serial..." onkeyup="handleSearch()">
</div>
<div class="d-flex gap-2">
<button class="btn btn-success fw-bold text-white shadow-sm" onclick="triggerDownload()"><i class="bi bi-file-earmark-excel"></i> Export</button>
<button class="btn btn-primary fw-bold shadow-sm" onclick="openModal('add')"><i class="bi bi-plus-lg"></i> Add Asset</button>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-4" id="categoryPillsContainer" style="overflow-x: auto; padding-bottom: 8px;"></div>
<div id="loading" class="text-center my-5"><div class="spinner-border text-primary"></div></div>
<div id="tableContainer" style="display: none; width: 100%;">
<table id="assetTable" class="table w-100">
<thead><tr><th>Group</th><th>Card</th></tr></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- CHANGE PIN MODAL -->
<div class="modal fade" id="pinModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header p-3"><h6 class="modal-title fw-bold text-primary">Change PIN</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body p-4 bg-light">
<label class="fw-bold">Current PIN</label><input type="password" id="p_oldPin" class="form-control mb-3">
<label class="fw-bold">New PIN</label><input type="password" id="p_newPin" class="form-control mb-3">
<label class="fw-bold">Confirm New PIN</label><input type="password" id="p_confirmPin" class="form-control mb-2">
<div id="pinMessage" class="fw-bold text-center small"></div>
</div>
<div class="modal-footer p-3 bg-white"><button class="btn btn-primary w-100 fw-bold" id="pinSaveBtn" onclick="submitPinChange()">Update PIN</button></div>
</div>
</div>
</div>
<!-- EXPORT MODAL -->
<div class="modal fade" id="downloadModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header p-3"><h6 class="modal-title fw-bold text-primary">Export Options</h6><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body p-3 bg-light">
<label class="fw-bold mb-2">Export Scope:</label>
<select id="exportScope" class="form-select mb-3">
<option value="current">Current Dashboard View</option>
<option value="all">All Inventory</option>
<option value="old">Only 5+ Year PCs</option>
</select>
<label class="fw-bold mb-2">Select Target Branches:</label>
<div class="form-check mb-2 pb-2 border-bottom"><input class="form-check-input" type="checkbox" id="selectAllBranches" checked onchange="toggleAllBranches(this)"><label class="form-check-label fw-bold text-primary small" for="selectAllBranches">Select All Branches</label></div>
<div id="branchCheckboxes" style="max-height: 220px; overflow-y: auto;"></div>
</div>
<div class="modal-footer p-3 bg-white"><button class="btn btn-success w-100 fw-bold text-white" onclick="executeDownload()">Download Excel</button></div>
</div>
</div>
</div>
<!-- ASSET MODAL -->
<div class="modal fade" id="assetModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-scrollable modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header p-4"><h5 class="modal-title fw-bold m-0" id="modalTitle">Asset Details</h5><button type="button" class="btn-close" onclick="closeModal()"></button></div>
<div class="modal-body bg-light p-4">
<input type="hidden" id="formAction"> <input type="hidden" id="editIndex">
<div id="hiddenReader" style="display:none;"></div>
<input type="file" id="barcodeNativeCamera" accept="image/*" capture="environment" style="display:none;">
<datalist id="suggestionList"></datalist>
<div class="accordion" id="assetAccordion">
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#colId">1. ID & Status</button></h2>
<div id="colId" class="accordion-collapse collapse show"><div class="accordion-body">
<div class="mb-3"><label class="fw-bold">Barcode ID *</label>
<div class="input-group"><input type="text" id="f_barcode" class="form-control req-field"><button class="btn btn-primary" type="button" id="scanBtn" onclick="document.getElementById('barcodeNativeCamera').click()"><i class="bi bi-camera-fill"></i></button></div>
</div>
<div class="mb-3"><label class="fw-bold" id="lbl_ciName">CI Name (Hostname) *</label><input type="text" id="f_ciName" class="form-control req-field"></div>
<div class="mb-3"><label class="fw-bold">Asset State *</label>
<select id="f_assetState" class="form-select req-field"><option value="In Use">In Use</option><option value="Repairing">Repairing</option><option value="Broken">Broken</option><option value="Request Deletion">Request Deletion</option></select>
</div>
<div class="row g-2 mb-0">
<div class="col-4"><label class="fw-bold text-truncate" style="font-size: 11px;">Label ខ្សែបណ្តាញ</label><select id="f_lblNetwork" class="form-select px-2 py-1" style="font-size: 13px;"><option value="" disabled selected></option><option value="មាន">មាន</option><option value="គ្មាន">គ្មាន</option><option value="មិនទាន់រៀបចំ">មិនទាន់រៀបចំ</option></select></div>
<div class="col-4"><label class="fw-bold text-truncate" style="font-size: 11px;">ស្លាកស្តង់ដារ</label><select id="f_lblStandard" class="form-select px-2 py-1" style="font-size: 13px;"><option value="" disabled selected></option><option value="មាន">មាន</option><option value="គ្មាន">គ្មាន</option><option value="រលុប">រលុប</option></select></div>
<div class="col-4"><label class="fw-bold text-truncate" style="font-size: 11px;">ខ្សែ Seal</label><select id="f_lblSeal" class="form-select px-2 py-1" style="font-size: 13px;"><option value="" disabled selected></option><option value="មាន">មាន</option><option value="គ្មាន">គ្មាន</option><option value="ដាច់">ដាច់</option></select></div>
</div>
</div></div>
</div>
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colHard">2. Hardware Specs</button></h2>
<div id="colHard" class="accordion-collapse collapse"><div class="accordion-body">
<div class="mb-2"><label class="fw-bold">Product Type</label>
<select id="f_prodType" class="form-select" onchange="toggleDynamicFields()">
<option value="Terminal Computer">Terminal Computer</option><option value="Workstation">Workstation</option>
<option value="Monitor">Monitor</option><option value="USB Printer">USB Printer</option>
<option value="Network Printer">Network Printer</option><option value="Barcode Reader">Barcode Reader</option>
<option value="IP Phone">IP Phone</option><option value="Scanner">Scanner</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-2"><label class="fw-bold">Model</label><input type="text" id="f_model" class="form-control"></div>
<div class="mb-2"><label class="fw-bold" id="lbl_serviceTag">Service Tag (Serial)</label><input type="text" id="f_serviceTag" class="form-control"></div>
<div class="row g-2 mb-0" id="pcHardwareFields"><div class="col-6"><label class="fw-bold">Processor</label><input type="text" id="f_processor" class="form-control"></div><div class="col-6"><label class="fw-bold">Memory (RAM)</label><input type="text" id="f_memory" class="form-control"></div></div>
</div></div>
</div>
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colLoc">3. Location & Network</button></h2>
<div id="colLoc" class="accordion-collapse collapse"><div class="accordion-body">
<div class="mb-2"><label class="fw-bold">Department / Branch *</label><input type="text" id="f_department" class="form-control req-field"></div>
<div class="row g-2 mb-2"><div class="col-6"><label class="fw-bold">Building</label><input type="text" id="f_locationName" class="form-control"></div><div class="col-6"><label class="fw-bold">Floor</label><input type="text" id="f_floor" class="form-control"></div></div>
<div class="row g-2 mb-2"><div class="col-6"><label class="fw-bold">Room Name</label><input type="text" id="f_roomName" class="form-control"></div><div class="col-6"><label class="fw-bold">Table Num</label><input type="text" id="f_tableNum" class="form-control"></div></div>
<div class="row g-2 mb-0"><div class="col-6"><label class="fw-bold">Switch ID</label><input type="text" id="f_switchId" class="form-control"></div><div class="col-6"><label class="fw-bold">Switch Port</label><input type="text" id="f_switchPort" class="form-control"></div></div>
<input type="hidden" id="f_roomNum"> <input type="hidden" id="f_connType" value="Local"> <input type="hidden" id="f_domain" value="TAX.LOCAL">
</div></div>
</div>
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colUser">4. User Assignment</button></h2>
<div id="colUser" class="accordion-collapse collapse"><div class="accordion-body">
<div class="row g-2 mb-2"><div class="col-4"><label class="fw-bold">Staff ID</label><input type="text" id="f_staffId" class="form-control"></div><div class="col-8"><label class="fw-bold">User Name *</label><input type="text" id="f_user" class="form-control req-field"></div></div>
<div class="mb-0"><label class="fw-bold">Position</label><input type="text" id="f_position" class="form-control"></div>
</div></div>
</div>
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colLife">5. Lifecycle Details</button></h2>
<div id="colLife" class="accordion-collapse collapse"><div class="accordion-body">
<div class="row g-2 mb-2"><div class="col-6"><label class="fw-bold">Acquisition Date</label><input type="date" id="f_acqDate" class="form-control"></div><div class="col-6"><label class="fw-bold">Warranty Expiry</label><input type="date" id="f_warrantyDate" class="form-control"></div></div>
<div class="row g-2 mb-0"><div class="col-6"><label class="fw-bold">Vendor</label><input type="text" id="f_vendor" class="form-control"></div><div class="col-6"><label class="fw-bold">Source</label><input type="text" id="f_source" class="form-control"></div></div>
<input type="hidden" id="f_regDate"> <input type="hidden" id="f_remark">
</div></div>
</div>
<div class="accordion-item">
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#colConnect"><i class="bi bi-link-45deg me-2 text-primary"></i>6. Connected Devices</button></h2>
<div id="colConnect" class="accordion-collapse collapse"><div class="accordion-body">
<div class="mb-0">
<label class="fw-bold">Connected Asset (Hostname / Service Tag)</label>
<input type="text" id="f_connectedAsset" list="suggestionList" class="form-control" placeholder="Search or type manually...">
</div>
</div></div>
</div>
<div class="accordion-item" id="historyAccordionWrapper" style="display:none;">
<h2 class="accordion-header"><button class="accordion-button collapsed text-success" type="button" data-bs-toggle="collapse" data-bs-target="#colHistory"><i class="bi bi-clock-history me-2"></i>7. Asset Edit History</button></h2>
<div id="colHistory" class="accordion-collapse collapse"><div class="accordion-body">
<div id="historyContainer" style="max-height: 300px; overflow-y: auto;" class="small text-muted text-center">Loading timeline...</div>
</div></div>
</div>
</div>
<div class="mt-4 p-3 bg-white rounded shadow-sm border">
<h6 class="fw-bold mb-3"><i class="bi bi-shield-check text-primary me-1"></i> Mandatory Audit Trail</h6>
<div class="mb-3"><label class="fw-bold">Reason for Change *</label><input type="text" id="f_reason" class="form-control req-field"></div>
<div class="mb-0"><label class="fw-bold">Live Photo Proof (Optional)</label><input type="file" id="f_auditPhoto" accept="image/*" capture="environment" class="form-control"></div>
</div>
<div id="statusMessage" class="fw-bold mt-3 text-center"></div>
</div>
<div class="modal-footer p-3 bg-white d-flex gap-2">
<button type="button" class="btn btn-light flex-grow-1" onclick="closeModal()">Cancel</button>
<button type="button" class="btn btn-primary flex-grow-1" id="saveBtn" onclick="saveData()">Save Record</button>
</div>
</div>
</div>
</div>
<!-- JS -->
<script>
// ==========================================
// CLOUDFLARE API ENDPOINT CONFIGURATION
// ==========================================
const API_URL = "https://script.google.com/macros/s/AKfycbxkp0v8Vfdtanrb8HknIB04yQN-FM9chAfsQMhHH0UIHwsnlYHlVBGZUg3G-TmZhWNF/exec";
function callApi(action, payload = {}) {
payload.action = action;
return fetch(API_URL, {
method: 'POST',
body: JSON.stringify(payload)
})
.then(res => res.json())
.catch(err => {
console.error("API Call Error:", err);
return { success: false, error: err.message };
});
}
let assetModal, downloadModal, pinModal, forgotPinModal, html5QrCode, userLat=null, userLng=null, fullDataset=[];
let activeCategory = 'All';
let collapsedGroups = {};
let dataTableInstance = null;
const categories = [
{ id: 'All', icon: 'bi-grid-fill', label: 'All Assets' },
{ id: 'Monitor', icon: 'bi-display', label: 'Monitor' },
{ id: 'Terminal Computer', icon: 'bi-pc-display-horizontal', label: 'Terminal Computer' },
{ id: 'USB Printer', icon: 'bi-printer', label: 'USB Printer' },
{ id: 'Network Printer', icon: 'bi-printer-fill', label: 'Network Printer' },
{ id: 'Barcode Reader', icon: 'bi-upc-scan', label: 'Barcode Reader' },
{ id: 'IP Phone', icon: 'bi-telephone-fill', label: 'IP Phone' },
{ id: 'Scanner', icon: 'bi-qr-code-scan', label: 'Scanner' },
{ id: '5+ Years', icon: 'bi-flag-fill', label: '5+ Year PCs', isWarning: true }
];
function toggleDynamicFields() {
const type = document.getElementById('f_prodType').value;
const isPC = type === 'Terminal Computer' || type === 'Workstation';
const isPhone = type === 'IP Phone';
const hardwareDiv = document.getElementById('pcHardwareFields');
const serialLabel = document.getElementById('lbl_serviceTag');
const ciNameLabel = document.getElementById('lbl_ciName');
if (isPC) {
hardwareDiv.style.display = 'flex';
serialLabel.innerText = 'Service Tag (Serial)';
ciNameLabel.innerText = 'CI Name (Hostname) *';
} else if (isPhone) {
hardwareDiv.style.display = 'none';
serialLabel.innerText = 'MAC Address / Serial';
ciNameLabel.innerText = 'Extension / Hostname *';
} else {
hardwareDiv.style.display = 'none';
serialLabel.innerText = 'Serial Number';
ciNameLabel.innerText = 'CI Name / Device ID *';
}
}
function safeDate(val) {
if (!val) return '';
let str = val.toString();
if (str.includes('T')) return str.split('T')[0];
if (str.includes(' ')) return str.split(' ')[0];
return str;
}
$.fn.dataTable.ext.search.push(
function(settings, data, dataIndex) {
let row = fullDataset[dataIndex];
if (!row) return true;
if (activeCategory !== 'All') {
if (activeCategory === '5+ Years') {
if (!checkIsOldPC(row)) return false;
} else {
if ((row.productType || "").trim() !== activeCategory) return false;
}
}
return true;
}
);
window.onload = function() {
assetModal = new bootstrap.Modal(document.getElementById('assetModal'));
downloadModal = new bootstrap.Modal(document.getElementById('downloadModal'));
pinModal = new bootstrap.Modal(document.getElementById('pinModal'));
forgotPinModal = new bootstrap.Modal(document.getElementById('forgotPinModal'));
html5QrCode = new Html5Qrcode("hiddenReader");
document.getElementById('barcodeNativeCamera').addEventListener('change', function(e) {
if (e.target.files.length == 0) return;
const scanBtn = document.getElementById('scanBtn'); scanBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
html5QrCode.scanFile(e.target.files[0], false).then(decodedText => {
document.getElementById('f_barcode').value = decodedText; document.getElementById('f_barcode').classList.remove('is-invalid');
scanBtn.innerHTML = '<i class="bi bi-check-lg"></i>'; setTimeout(() => scanBtn.innerHTML = '<i class="bi bi-camera-fill"></i>', 2000);
}).catch(err => { scanBtn.innerHTML = '<i class="bi bi-camera-fill"></i>'; alert("Barcode not detected."); }); e.target.value = '';
});
if (localStorage.getItem('gdt_remember') === 'true') {
document.getElementById('officerId').value = localStorage.getItem('gdt_id') || "";
document.getElementById('pinCode').value = localStorage.getItem('gdt_pin') || "";
document.getElementById('rememberMe').checked = true;
}
if (sessionStorage.getItem('officerId')) showDashboard(); else showLogin();
};
function attemptLogin() {
const id = document.getElementById('officerId').value.trim(), pin = document.getElementById('pinCode').value.trim(), btn = document.getElementById('loginBtn');
if(!id || !pin) return document.getElementById('loginMessage').innerHTML = "<span class='text-danger'>Enter ID and PIN.</span>";
btn.disabled = true; btn.innerHTML = 'Authenticating...';
callApi('verifyLogin', { officerId: id, pin: pin }).then(res => {
btn.disabled = false; btn.innerHTML = 'Authenticate';
if(res.success) {
if(document.getElementById('rememberMe').checked) {
localStorage.setItem('gdt_remember', 'true'); localStorage.setItem('gdt_id', id); localStorage.setItem('gdt_pin', pin);
} else { localStorage.removeItem('gdt_remember'); localStorage.removeItem('gdt_id'); localStorage.removeItem('gdt_pin'); }
sessionStorage.setItem('officerId', res.officerId); sessionStorage.setItem('name', res.name);
sessionStorage.setItem('role', res.role); sessionStorage.setItem('branch', res.branch);
showDashboard();
} else document.getElementById('loginMessage').innerHTML = `<span class='text-danger'>${res.message}</span>`;
}).catch(err => { btn.disabled = false; btn.innerHTML = 'Authenticate'; document.getElementById('loginMessage').innerHTML = `<span class='text-danger'>Error: ${err.message}</span>`; });
}
function logout() { sessionStorage.clear(); if (localStorage.getItem('gdt_remember') !== 'true') { document.getElementById('officerId').value = ''; document.getElementById('pinCode').value = ''; } document.getElementById('loginMessage').innerText = ''; showLogin(); }
function showLogin() { document.getElementById('dashboardView').style.display = 'none'; document.getElementById('loginView').style.display = 'flex'; }
function showDashboard() {
document.getElementById('loginView').style.display = 'none'; document.getElementById('dashboardView').style.display = 'block';
document.getElementById('welcomeName').innerText = sessionStorage.getItem('name');
document.getElementById('assignedBranch').innerText = `${sessionStorage.getItem('branch')} • ${sessionStorage.getItem('role')}`;
loadTableData();
}
function populateSuggestions() {
const dl = document.getElementById('suggestionList'); dl.innerHTML = ''; let opts = new Set();
fullDataset.forEach(i => {
if (i.rawRow[9] && i.rawRow[9] !== 'N/A') opts.add(i.rawRow[9].toString().trim());
if (i.rawRow[18]) opts.add(i.rawRow[18].toString().trim());
});
opts.forEach(val => { let opt = document.createElement('option'); opt.value = val; dl.appendChild(opt); });
}
function checkIsOldPC(row) {
const type = (row.productType || "").toLowerCase();
if (!type.includes('workstation') && !type.includes('terminal') && !type.includes('pc')) return false;
if (!row.rawRow[12]) return false;
const acqDate = new Date(row.rawRow[12]);
if (isNaN(acqDate.getTime())) return false;
const ageMonths = (new Date() - acqDate) / (1000 * 60 * 60 * 24 * 30.44);
return ageMonths > 60;
}
function loadTableData() {
document.getElementById('tableContainer').style.display = 'none';
document.getElementById('loading').style.display = 'block';
callApi('getSecureReportData', { officerId: sessionStorage.getItem('officerId') })
.then(data => {
if (data && data.error) {
document.getElementById('loading').innerHTML = `<div class="text-danger fw-bold"><i class="bi bi-exclamation-triangle"></i> Backend Error: ${data.error}</div>`;
return;
}
if (!data || data.length === 0) {
document.getElementById('loading').innerHTML = `<div class="text-danger fw-bold"><i class="bi bi-database-x"></i> 0 Rows Found. Did your Google Sheet Tab name change?</div>`;
} else {
document.getElementById('loading').style.display = 'none';
}
fullDataset = data || [];
populateSuggestions();
updatePillCounts();
renderTableOnce(fullDataset);
document.getElementById('tableContainer').style.display = 'block';
})
.catch(err => {
document.getElementById('loading').innerHTML = `<div class="text-danger fw-bold"><i class="bi bi-x-octagon-fill"></i> Connection Error: ${err.message}</div>`;
});
}
function handleSearch() {
let val = document.getElementById('globalSearch').value;
if(dataTableInstance) dataTableInstance.search(val).draw();
updatePillCounts();
}
function updatePillCounts() {
const term = document.getElementById('globalSearch').value.toLowerCase().trim();
let counts = { 'All': 0, 'Monitor': 0, 'Terminal Computer': 0, 'USB Printer': 0, 'Network Printer': 0, 'Barcode Reader': 0, 'IP Phone': 0, 'Scanner': 0, '5+ Years': 0 };
fullDataset.forEach(r => {
let matchesSearch = true;
if(term) {
matchesSearch = (r.barcode||"").toLowerCase().includes(term) ||
(r.ciName||"").toLowerCase().includes(term) ||
(r.user||"").toLowerCase().includes(term) ||
(r.rawRow[18]||"").toLowerCase().includes(term) ||
(r.rawRow[30]||"").toLowerCase().includes(term);
}
if(matchesSearch) {
counts['All']++;
let type = (r.productType||"").trim();
if (counts[type] !== undefined) counts[type]++;
if (checkIsOldPC(r)) counts['5+ Years']++;
}
});
const pillCont = document.getElementById('categoryPillsContainer'); pillCont.innerHTML = '';
categories.forEach(cat => {
if (cat.id !== 'All' && counts[cat.id] === 0 && !cat.isWarning) return;
const isAct = activeCategory === cat.id ? 'active' : '';
const isWarn = cat.isWarning ? 'warning-pill' : '';
pillCont.innerHTML += `<div class="category-pill ${isWarn} ${isAct}" onclick="setCategory('${cat.id}')"><i class="bi ${cat.icon}"></i> ${cat.label} <span class="badge-count">${counts[cat.id]}</span></div>`;
});
}
function setCategory(catId) {
activeCategory = (activeCategory === catId) ? 'All' : catId;
updatePillCounts();
if(dataTableInstance) {
Object.keys(collapsedGroups).forEach(k => collapsedGroups[k] = true);
dataTableInstance.draw();
}
}
function renderTableOnce(data) {
if($.fn.DataTable.isDataTable('#assetTable')) $('#assetTable').DataTable().destroy();
const tbody = document.getElementById('tableBody'); tbody.innerHTML = '';
data.forEach((row, index) => {
let isOld = checkIsOldPC(row);
let warnBadge = isOld ? `<span class="warning-badge"><i class="bi bi-flag-fill"></i> 5+ Years</span>` : '';
let oldClass = isOld ? 'old-pc-alert' : '';
let badge = '<span class="custom-badge" style="background:#ecfdf5;color:#059669;">In Use</span>';
if(row.assetState==='Broken') badge='<span class="custom-badge" style="background:#fef2f2;color:#dc2626;">Broken</span>';
if(row.assetState==='Repairing') badge='<span class="custom-badge" style="background:#fffbeb;color:#b45309;">Repairing</span>';
if(row.assetState==='Request Deletion') badge='<span class="custom-badge" style="background:#f1f5f9;color:#475569;">ស្នើសុំរំលាយចោល</span>';
let rawDpt = (row.department||"").toString();
let dpt = rawDpt.replace(/[\s\u200B-\u200D\uFEFF\u00A0]/g, '');
dpt = dpt.replace(/មណ្ឌលគីរី/g, 'មណ្ឌលគិរី').replace(/បៃលិន/g, 'ប៉ៃលិន').replace(/ព្រះសីហនុ/g, 'ព្រះសីហនុ')
.replace(/រដ្ឋបាលនិងកិច្ចការទូទៅ/g, 'រដ្ឋបាលនិងកិច្ចការទូទៅ').replace(/^ន\./g, 'នាយកដ្ឋាន');
if (dpt.includes('នីតិកម្ម') && (dpt.includes('គោលនយោ') || dpt.includes('គោល'))) dpt = 'នាយកដ្ឋាននីតិកម្មគោលនយោបាយសារពើពន្ធនិងសហប្រតិបត្តិការពន្ធដារអន្តរជាតិ';
if (dpt.startsWith("នាយកដ្ឋាន")) dpt = "នាយកដ្ឋាន " + dpt.substring(9);
else if (dpt.startsWith("សាខាពន្ធដារខេត្ត")) dpt = "សាខាពន្ធដារខេត្ត " + dpt.substring(16);
else if (dpt.startsWith("សាខាពន្ធដារខណ្ឌ")) dpt = "សាខាពន្ធដារខណ្ឌ " + dpt.substring(15);
else if (dpt.startsWith("ទីចាត់ការ")) dpt = "ទីចាត់ការ " + dpt.substring(9);
else if (dpt.startsWith("ខេត្ត")) dpt = "ខេត្ត " + dpt.substring(5);
else if (dpt.startsWith("ខណ្ឌ")) dpt = "ខណ្ឌ " + dpt.substring(4);
let cat = "🏢 HQ Department";
if(dpt.includes("ខណ្ឌ")) cat = "📍 Khan Branch";
else if(dpt.includes("ខេត្ត")) cat = "🗺 Provincial Branch";
else if(!dpt || dpt.toLowerCase()==="notassigned") { cat = "⚠️ Unassigned"; dpt = "Not Assigned"; }
let finalGroup = `${cat} — ${dpt}`;
let ciDisplay = (row.ciName && row.ciName !== 'N/A') ? row.ciName : row.barcode;
let displayModel = (row.productType || 'N/A') + ' ' + (row.model || '');
let serial = row.rawRow[18] || 'No Serial';
let connected = row.rawRow[30] ? `<span class="ms-3"><i class="bi bi-link-45deg" style="color: #64748b; font-size: 14px;"></i> តភ្ជាប់៖ <strong>${row.rawRow[30]}</strong></span>` : '';
let acqDateText = (isOld && row.rawRow[12]) ? `<div class="ms-3 text-danger fw-bold"><i class="bi bi-calendar-x" style="font-size: 13px; margin-right: 4px;"></i> ទិញតាំងពី៖ ${safeDate(row.rawRow[12])}</div>` : '';
let cardHtml = `
<div class="asset-card ${oldClass}" onclick="openModal('edit', ${index})">
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-bold" style="color: #1e293b; font-size: 15px;">${ciDisplay} <span class="text-muted fw-normal ms-2" style="font-size: 12px;">| Barcode: ${row.barcode}</span></div>
<div>${warnBadge}${badge}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div style="font-size: 13px; color: #1e293b;">ផលិតផល៖ <strong>${displayModel}</strong> <span class="ms-1 text-muted" style="font-size: 12px;">(${serial})</span></div>
<i class="bi bi-arrow-right" style="color: #94a3b8; font-size: 16px;"></i>
</div>
<div class="d-flex justify-content-start align-items-center flex-wrap" style="font-size: 13px; color: #1e293b; gap: 8px;">
<div><i class="bi bi-person-fill" style="color: #64748b; font-size: 14px; margin-right: 4px;"></i> អ្នកកាន់៖ <strong>${row.user}</strong></div>
<div class="ms-3"><i class="bi bi-building" style="color: #64748b; font-size: 14px; margin-right: 4px;"></i> ទីតាំង៖ <strong>${dpt}</strong></div>
${connected}
${acqDateText}
</div>
</div>`;
let tr = document.createElement('tr');
tr.innerHTML = `<td>${finalGroup}</td><td>${cardHtml}</td>`;
tbody.appendChild(tr);
});
dataTableInstance = $('#assetTable').DataTable({
paging: false,
responsive: true,
order: [[0, 'asc']],
deferRender: true,
columnDefs: [ { targets: [0], visible: false }, { targets: [1], className: 'p-0 border-0' } ],
rowGroup: { dataSrc: 0, startRender: function (rows, group) {
if(collapsedGroups[group] === undefined) collapsedGroups[group] = true;
rows.nodes().each(function (r) { r.style.display = collapsedGroups[group] ? 'none' : ''; });
let tr = document.createElement('tr'), td = document.createElement('td');
td.colSpan = 1; td.className = 'group-header'; td.setAttribute('data-name', group);
td.innerHTML = `<i class="bi ${collapsedGroups[group]?'bi-folder':'bi-folder2-open'} me-2 text-primary"></i> ${group} <span class="badge badge-count-folder ms-2">${rows.count()}</span>`; tr.appendChild(td); return tr;
}},
dom: "t"
});
$('#assetTable tbody').off('click', 'tr.dtrg-start').on('click', 'tr.dtrg-start', function () {
let name = $(this).find('td.group-header').attr('data-name'); if(name !== undefined) { collapsedGroups[name] = !collapsedGroups[name]; dataTableInstance.draw(false); }
});
}
function openModal(action, index = -1) {
document.getElementById('editIndex').value = index;
document.querySelectorAll('input, select').forEach(el => { el.value = ''; el.classList.remove('is-invalid'); });
document.getElementById('statusMessage').innerText = ''; document.getElementById('formAction').value = action;
$('.accordion-collapse').collapse('hide');$('#colId').collapse('show');
const ro = ['f_acqDate', 'f_warrantyDate', 'f_vendor', 'f_source'];
if(action === 'add') {
document.getElementById('modalTitle').innerText = 'Register Asset'; document.getElementById('f_department').value = sessionStorage.getItem('branch');
document.getElementById('historyAccordionWrapper').style.display = 'none';
let bc = document.getElementById('f_barcode'); bc.removeAttribute('readonly'); bc.style.pointerEvents='auto'; bc.style.backgroundColor='#fff';
ro.forEach(id => { let el=document.getElementById(id); el.removeAttribute('readonly'); el.style.pointerEvents='auto'; el.style.backgroundColor='#fff'; });
document.getElementById('f_lblNetwork').value = ''; document.getElementById('f_lblStandard').value = ''; document.getElementById('f_lblSeal').value = '';
document.getElementById('f_prodType').value = 'Terminal Computer';
toggleDynamicFields();
} else {
document.getElementById('modalTitle').innerText = 'Update Asset';
let bc = document.getElementById('f_barcode'); bc.setAttribute('readonly', 'true'); bc.style.pointerEvents='none'; bc.style.backgroundColor='#e2e8f0';
ro.forEach(id => { let el=document.getElementById(id); el.setAttribute('readonly', 'true'); el.style.pointerEvents='none'; el.style.backgroundColor='#e2e8f0'; });
const r = fullDataset[index].rawRow; const tgt = r[10];
document.getElementById('historyAccordionWrapper').style.display = 'block';
document.getElementById('historyContainer').innerHTML = '<div class="spinner-border text-primary spinner-border-sm mt-2"></div> Loading timeline...';
callApi('getAssetHistory', { barcode: tgt }).then(h => {
let c = document.getElementById('historyContainer');
if(!h || h.length===0) return c.innerHTML = "<p class='mt-2'>No history found for this asset.</p>";
let html = "<ul class='list-unstyled m-0 text-start'>";
h.forEach(l => {
let photoLink = (l.photo && l.photo !== 'No Photo' && l.photo !== 'Photo Error') ? `<div class="mt-2"><a href="${l.photo}" target="_blank" class="small text-decoration-none fw-bold"><i class="bi bi-image me-1"></i>View Photo Proof</a></div>` : '';
let changesHTML = l.changes;
if (!changesHTML) {
let leg = [];
if (l.oldUser && l.newUser && l.oldUser !== l.newUser && l.oldUser !== 'N/A') leg.push(`• <b>User:</b> <del style="color:#ef4444;">${l.oldUser}</del> → <span style="color:#10b981; font-weight:600;">${l.newUser}</span>`);
if (l.oldLoc && l.newLoc && l.oldLoc !== l.newLoc && l.oldLoc !== 'N/A' && l.oldLoc !== 'None') leg.push(`• <b>Location:</b> <del style="color:#ef4444;">${l.oldLoc}</del> → <span style="color:#10b981; font-weight:600;">${l.newLoc}</span>`);
if (leg.length > 0) changesHTML = leg.join("<br>");
}
let changesDiv = changesHTML ? `<div class="timeline-change-box mt-2">${changesHTML}</div>` : `<div class="timeline-change-box mt-2 text-muted"><i>No field changes recorded.</i></div>`;
html += `<li class='mb-3 pb-3 border-bottom'><div class="d-flex justify-content-between text-muted" style="font-size: 11.5px;"><span><i class="bi bi-clock me-1"></i>${l.date}</span><span class="fw-bold text-primary">${l.officer}</span></div><div class="fw-bold mt-1 text-dark" style="font-size: 13px;">Reason: ${l.reason}</div>${changesDiv}${photoLink}</li>`;
});
c.innerHTML = html + "</ul>";
});
document.getElementById('f_department').value=r[0]; document.getElementById('f_locationName').value=r[1]; document.getElementById('f_floor').value=r[2];
document.getElementById('f_roomNum').value=r[3]; document.getElementById('f_roomName').value=r[4]; document.getElementById('f_tableNum').value=r[5];
document.getElementById('f_user').value=r[6]; document.getElementById('f_staffId').value=r[7]; document.getElementById('f_position').value=r[8];
document.getElementById('f_ciName').value=r[9]; document.getElementById('f_barcode').value=r[10]; document.getElementById('f_assetState').value=r[11];
try{ document.getElementById('f_acqDate').value = safeDate(r[12]); }catch(e){}
try{ document.getElementById('f_warrantyDate').value = safeDate(r[13]); }catch(e){}
let pType = r[14] ? r[14].toString().trim() : '';
if(Array.from(document.getElementById('f_prodType').options).some(o => o.value === pType)) document.getElementById('f_prodType').value = pType; else document.getElementById('f_prodType').value = 'Other';
document.getElementById('f_model').value=r[15]; document.getElementById('f_processor').value=r[16];
document.getElementById('f_memory').value=r[17]; document.getElementById('f_serviceTag').value=r[18]; document.getElementById('f_switchId').value=r[19];
document.getElementById('f_switchPort').value=r[20]; document.getElementById('f_connType').value=r[21]; document.getElementById('f_domain').value=r[22];
document.getElementById('f_source').value=r[23]; document.getElementById('f_vendor').value=r[24]; document.getElementById('f_regDate').value=r[25]; document.getElementById('f_remark').value=r[26];
document.getElementById('f_lblNetwork').value=r[27]||''; document.getElementById('f_lblStandard').value=r[28]||''; document.getElementById('f_lblSeal').value=r[29]||'';
document.getElementById('f_connectedAsset').value=r[30]||'';
toggleDynamicFields();
}
navigator.geolocation.getCurrentPosition(p => { userLat=p.coords.latitude; userLng=p.coords.longitude; }, e => {}); assetModal.show();
}
function closeModal() { assetModal.hide(); }
function saveData() {
let valid = true; document.querySelectorAll('.req-field').forEach(el => { if(!el.value.trim()){ el.classList.add('is-invalid'); valid=false; } else el.classList.remove('is-invalid'); });
if(!valid) return document.getElementById('statusMessage').innerHTML = "<span class='text-danger'>Fill all required fields.</span>";
document.getElementById('saveBtn').disabled = true; document.getElementById('statusMessage').innerHTML = "<span class='text-primary'>Saving record & tracking changes...</span>";
const p = {
department: document.getElementById('f_department').value, locationName: document.getElementById('f_locationName').value, floor: document.getElementById('f_floor').value, roomNum: document.getElementById('f_roomNum').value, roomName: document.getElementById('f_roomName').value, tableNum: document.getElementById('f_tableNum').value, user: document.getElementById('f_user').value, staffId: document.getElementById('f_staffId').value, position: document.getElementById('f_position').value, ciName: document.getElementById('f_ciName').value, barcode: document.getElementById('f_barcode').value, assetState: document.getElementById('f_assetState').value, acqDate: document.getElementById('f_acqDate').value, warrantyDate: document.getElementById('f_warrantyDate').value, prodType: document.getElementById('f_prodType').value, model: document.getElementById('f_model').value, processor: document.getElementById('f_processor').value, memory: document.getElementById('f_memory').value, serviceTag: document.getElementById('f_serviceTag').value, switchId: document.getElementById('f_switchId').value, switchPort: document.getElementById('f_switchPort').value, connType: document.getElementById('f_connType').value, domain: document.getElementById('f_domain').value, source: document.getElementById('f_source').value, vendor: document.getElementById('f_vendor').value, regDate: document.getElementById('f_regDate').value, remark: document.getElementById('f_remark').value, lblNetwork: document.getElementById('f_lblNetwork').value, lblStandard: document.getElementById('f_lblStandard').value, lblSeal: document.getElementById('f_lblSeal').value, connectedAsset: document.getElementById('f_connectedAsset').value
};
const fileInput = document.getElementById('f_auditPhoto');
const execSave = (b64) => {
const act = document.getElementById('formAction').value; const idx = document.getElementById('editIndex').value;
const pay = { action: act, reason: document.getElementById('f_reason').value, data: p, imageBase64: b64, lat: userLat, lng: userLng, officerId: sessionStorage.getItem('officerId') };
const rData = [p.department, p.locationName, p.floor, p.roomNum, p.roomName, p.tableNum, p.user, p.staffId, p.position, p.ciName, p.barcode, p.assetState, p.acqDate, p.warrantyDate, p.prodType, p.model, p.processor, p.memory, p.serviceTag, p.switchId, p.switchPort, p.connType, p.domain, p.source, p.vendor, p.regDate, p.remark, p.lblNetwork, p.lblStandard, p.lblSeal, p.connectedAsset];
if(act === 'edit') { fullDataset[idx].department = p.department; fullDataset[idx].productType = p.prodType; fullDataset[idx].ciName = p.ciName; fullDataset[idx].user = p.user; fullDataset[idx].assetState = p.assetState; fullDataset[idx].rawRow = rData; }
else { fullDataset.push({ department: p.department, productType: p.prodType, barcode: p.barcode, ciName: p.ciName, user: p.user, assetState: p.assetState, rawRow: rData }); }
closeModal(); populateSuggestions(); updatePillCounts(); renderTableOnce(fullDataset); document.getElementById('saveBtn').disabled = false;
callApi('saveAssetSecure', pay).then(res => { if(!res.success) alert("Error: " + res.message); });
};
if(fileInput.files.length > 0) {
const r = new FileReader(); r.onload = e => { const img = new Image(); img.src = e.target.result; img.onload = () => { const cvs = document.createElement("canvas"), ctx = cvs.getContext("2d"), s = 800/img.width; cvs.width=800; cvs.height=img.height*s; ctx.drawImage(img,0,0,cvs.width,cvs.height); execSave(cvs.toDataURL("image/jpeg", 0.7)); }}; r.readAsDataURL(fileInput.files[0]);
} else execSave(null);
}
function submitForgotPin() {
const id = document.getElementById('fp_officerId').value.trim(), msg = document.getElementById('fpMessage'), btn = document.getElementById('fpSendBtn');
if(!id) return msg.innerHTML = "<span class='text-danger'>Enter Officer ID.</span>";
btn.disabled = true; msg.innerHTML = "<span class='text-primary'>Sending...</span>";
callApi('requestPasswordReset', { officerId: id }).then(res => {
btn.disabled = false;
if(res.success) { msg.innerHTML = `<span class='text-success'>${res.message}</span>`; setTimeout(() => forgotPinModal.hide(), 2500); }
else msg.innerHTML = `<span class='text-danger'>${res.message}</span>`;
}).catch(err => { btn.disabled = false; msg.innerHTML = `<span class='text-danger'>Error: ${err.message}</span>`; });
}
function openPinModal() { document.getElementById('p_oldPin').value=''; document.getElementById('p_newPin').value=''; document.getElementById('p_confirmPin').value=''; document.getElementById('pinMessage').innerText=''; pinModal.show(); }
function submitPinChange() {
const oldP = document.getElementById('p_oldPin').value.trim(), newP = document.getElementById('p_newPin').value.trim(), confP = document.getElementById('p_confirmPin').value.trim(), msg = document.getElementById('pinMessage'), btn = document.getElementById('pinSaveBtn');
if(!oldP || !newP || !confP) return msg.innerHTML = "<span class='text-danger'>All fields required.</span>";
if(newP !== confP) return msg.innerHTML = "<span class='text-danger'>PINs do not match.</span>";
btn.disabled = true; msg.innerHTML = "<span class='text-primary'>Saving...</span>";
callApi('changeUserPin', { officerId: sessionStorage.getItem('officerId'), oldPin: oldP, newPin: newP }).then(res => {
if(res.success) {
msg.innerHTML = "<span class='text-success'>PIN updated!</span>";
if(localStorage.getItem('gdt_remember') === 'true') localStorage.setItem('gdt_pin', newP);
setTimeout(() => { pinModal.hide(); btn.disabled = false; }, 1500);
} else { btn.disabled = false; msg.innerHTML = `<span class='text-danger'>${res.message}</span>`; }
});
}
function toggleAllBranches(src) { document.querySelectorAll('.branch-cb').forEach(cb => cb.checked = src.checked); }
function updateSelectAllStatus() { document.getElementById('selectAllBranches').checked = (document.querySelectorAll('.branch-cb').length === document.querySelectorAll('.branch-cb:checked').length); }
function triggerDownload() {
const role = sessionStorage.getItem('role').toLowerCase();
const c = document.getElementById('branchCheckboxes');
c.innerHTML = '';
document.getElementById('selectAllBranches').checked = true;
let bSet = new Set();
if (role.includes('admin')) {
fullDataset.forEach(i => { if(i.department) bSet.add(i.department.toString().trim()); });
} else {
bSet.add(sessionStorage.getItem('branch'));
}
Array.from(bSet).sort().forEach((b, idx) => {
let disabled = !role.includes('admin') ? 'onclick="return false;"' : '';
c.innerHTML += `<div class="form-check mb-2"><input class="form-check-input branch-cb" type="checkbox" value="${b}" id="cb_${idx}" checked onchange="updateSelectAllStatus()" ${disabled}><label class="form-check-label small" for="cb_${idx}">${b}</label></div>`;
});
downloadModal.show();
}
function executeDownload(forceB = null) {
let sel = []; let isAll = false;
if(forceB) sel = forceB; else { document.querySelectorAll('.branch-cb:checked').forEach(cb => sel.value && sel.push(cb.value)); isAll = document.getElementById('selectAllBranches').checked; downloadModal.hide(); }
if(sel.length===0 && !isAll) return alert("Select at least one branch.");
let scope = document.getElementById('exportScope') ? document.getElementById('exportScope').value : 'all';
let exp = fullDataset;
let scopeName = "Inventory";
if (scope === 'current') {
if (activeCategory === '5+ Years') {
exp = exp.filter(r => checkIsOldPC(r));
scopeName = "5PlusYears";
} else if (activeCategory !== 'All') {
exp = exp.filter(r => (r.productType||"").trim() === activeCategory);
scopeName = activeCategory.replace(/\s+/g, '');
}
const term = document.getElementById('globalSearch').value.toLowerCase().trim();
if (term) {
exp = exp.filter(r => {
return (r.barcode||"").toLowerCase().includes(term) ||
(r.ciName||"").toLowerCase().includes(term) ||
(r.user||"").toLowerCase().includes(term) ||
(r.rawRow[18]||"").toLowerCase().includes(term) ||
(r.rawRow[30]||"").toLowerCase().includes(term);
});
}
} else if (scope === 'old') {
exp = exp.filter(r => checkIsOldPC(r));
scopeName = "5PlusYears";
}
if(!isAll && !forceB) {
exp = exp.filter(i => sel.includes((i.department||"").toString().trim()));
} else if(forceB && forceB[0]!=="ALL") {
exp = exp.filter(i => (i.department||"").toString().trim() === forceB[0].trim());
}
let branchName = "Selected";
if (isAll || (forceB && forceB[0] === "ALL")) branchName = "AllBranches";
else if (sel.length === 1 || (forceB && forceB.length === 1)) branchName = sel[0] || forceB[0];
branchName = branchName.replace(/[\s/\\:*?"<>|]/g, '_');
let d = new Date();
let fName = `GDT_${scopeName}_${branchName}_${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}.xlsx`;
exp.sort((a, b) => {
let dA = (a.department || "").toString().trim();
let dB = (b.department || "").toString().trim();
return dA.localeCompare(dB);
});
let finalExportData = [];
let currentDept = null;
let deptCount = 0;
let grandTotal = 0;
const getEmptyRow = () => ({ "Department": "", "Location": "", "Floor": "", "Room Name": "", "User": "", "Staff ID": "", "Position": "", "CI Name": "", "Barcode": "", "Asset State": "", "Acq Date": "", "Warranty": "", "Product": "", "Model": "", "Processor": "", "Memory": "", "Serial": "", "Vendor": "", "Remark": "", "Label ខ្សែបណ្តាញ": "", "ស្លាកស្តង់ដារ": "", "ខ្សែ Seal": "", "Connected Asset": "" });
exp.forEach(i => {
let r = i.rawRow;
let dept = (r[0] || "").toString().trim() || "Unassigned";
if (currentDept !== dept) {
if (currentDept !== null) {
let subRow = getEmptyRow();
subRow["Department"] = `▶ SUBTOTAL: ${currentDept}`;
subRow["Location"] = `${deptCount} Asset(s)`;
finalExportData.push(subRow);
finalExportData.push(getEmptyRow());
}
deptCount = 0;
let headerRow = getEmptyRow();
headerRow["Department"] = `▼ GROUP: ${dept.toUpperCase()} ▼`;
finalExportData.push(headerRow);
}
currentDept = dept;
deptCount++;
grandTotal++;
finalExportData.push({
"Department": dept, "Location": r[1]||"", "Floor": r[2]||"", "Room Name": r[4]||"", "User": r[6]||"", "Staff ID": r[7]||"", "Position": r[8]||"", "CI Name": r[9]||"", "Barcode": r[10]||"", "Asset State": r[11]||"", "Acq Date": safeDate(r[12]), "Warranty": safeDate(r[13]), "Product": r[14]||"", "Model": r[15]||"", "Processor": r[16]||"", "Memory": r[17]||"", "Serial": r[18]||"", "Vendor": r[24]||"", "Remark": r[26]||"", "Label ខ្សែបណ្តាញ": r[27]||"", "ស្លាកស្តង់ដារ": r[28]||"", "ខ្សែ Seal": r[29]||"", "Connected Asset": r[30]||""
});
});
if (currentDept !== null) {
let subRow = getEmptyRow();
subRow["Department"] = `▶ SUBTOTAL: ${currentDept}`;
subRow["Location"] = `${deptCount} Asset(s)`;
finalExportData.push(subRow);
}
if (grandTotal > 0) {
finalExportData.push(getEmptyRow());
let gtRow = getEmptyRow();
gtRow["Department"] = "★ GRAND TOTAL ★";
gtRow["Location"] = `${grandTotal} Asset(s)`;
finalExportData.push(gtRow);
}
const ws = XLSX.utils.json_to_sheet(finalExportData);
const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Assets"); XLSX.writeFile(wb, fName);
}
</script>
</body>
</html>