document.addEventListener('alpine:init', () => { Alpine.data("dashboard", () => ({ // dashboard today: "", filter: { date1: "", date2: "" }, list: [], isLoading: false, counters: { Pend: 0, Coll: 0, Recv: 0, Inc: 0, Fin: 0, Total: 0 }, statusColor: { Pend: 'bg-white text-black font-bold', PartColl: 'bg-[#ff99aa] text-black font-bold', Coll: 'bg-[#d63031] text-white font-bold', PartRecv: 'bg-[#a0c0d9] text-black font-bold', Recv: 'bg-[#0984e3] text-white font-bold', Inc: 'bg-[#ffff00] text-black font-bold', Fin: 'bg-[#008000] text-white font-bold', }, statusRowBg: { Pend: 'bg-white text-black', PartColl: 'bg-[#ff99aa] text-black', Coll: 'bg-[#d63031] text-white', PartRecv: 'bg-[#a0c0d9] text-black', Recv: 'bg-[#0984e3] text-white', Inc: 'bg-[#ffff00] text-black', Fin: 'bg-[#008000] text-white', }, filterTable: "", filterKey: 'Total', statusMap: { Total: [], Pend: ['Pend'], Coll: ['Coll', 'PartColl'], Recv: ['Recv', 'PartRecv'], Inc: ['Inc'], Fin: ['Fin'], }, // Sorting & Pagination sortCol: 'REQDATE', sortAsc: false, currentPage: 1, pageSize: 30, sort(col) { if (this.sortCol === col) { this.sortAsc = !this.sortAsc; } else { this.sortCol = col; this.sortAsc = true; } }, nextPage() { if (this.currentPage < this.totalPages) this.currentPage++; }, prevPage() { if (this.currentPage > 1) this.currentPage--; }, get totalPages() { return Math.ceil(this.filtered.length / this.pageSize) || 1; }, get sorted() { return this.filtered.slice().sort((a, b) => { let modifier = this.sortAsc ? 1 : -1; if (a[this.sortCol] < b[this.sortCol]) return -1 * modifier; if (a[this.sortCol] > b[this.sortCol]) return 1 * modifier; return 0; }); }, get paginated() { const start = (this.currentPage - 1) * this.pageSize; const end = start + this.pageSize; return this.sorted.slice(start, end); }, init() { this.today = new Date().toISOString().slice(0, 10); // Production: default to today this.filter.date1 = this.today; this.filter.date2 = this.today; this.$watch('filterTable', () => { this.currentPage = 1; }); this.fetchList(); }, fetchList() { this.isLoading = true; this.list = []; let statusOrder = { Pend: 1, PartColl: 2, Coll: 3, PartRecv: 4, Recv: 5, Inc: 6, Fin: 7 }; let param = new URLSearchParams(this.filter).toString(); for (let k in this.counters) { this.counters[k] = 0; } fetch(`${BASEURL}/api/requests?${param}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }).then(res => res.json()).then(data => { this.list = data.data ?? []; this.filterKey = 'Total'; this.list.forEach(item => { if (this.counters[item.STATS] !== undefined) { this.counters[item.STATS]++; this.counters.Total++; } else { if (item.STATS == 'PartColl') { this.counters.Coll++; } else if (item.STATS == 'PartRecv') { this.counters.Recv++; } this.counters.Total++; } }); this.list.sort((a, b) => { let codeA = statusOrder[a.STATS] ?? 0; let codeB = statusOrder[b.STATS] ?? 0; return codeA - codeB; }); }).finally(() => { this.isLoading = false; }); }, reset() { this.filter.date1 = this.today; this.filter.date2 = this.today; this.fetchList(); }, isValidated(item) { return item.ISVAL == 1 && item.ISPENDING != 1; }, get filtered() { let filteredList = this.list; if (this.filterKey === 'Validated') { filteredList = filteredList.filter(item => this.isValidated(item)); } else { const validStatuses = this.statusMap[this.filterKey]; if (validStatuses.length > 0) { filteredList = filteredList.filter(item => validStatuses.includes(item.STATS)); } } if (this.filterTable) { const searchTerm = this.filterTable.toLowerCase(); filteredList = filteredList.filter(item => Object.values(item).some(value => String(value).toLowerCase().includes(searchTerm) ) ); } return filteredList; }, get validatedCount() { return this.list.filter(r => this.isValidated(r)).length; }, /* sample dialog */ item: '', isDialogSampleOpen: false, isSampleLoading: false, openSampleDialog(accessnumber) { this.isDialogSampleOpen = true; this.fetchItem(accessnumber) }, closeSampleDialog() { this.isDialogSampleOpen = false; }, fetchItem(accessnumber) { this.isSampleLoading = true; this.item = []; fetch(`${BASEURL}/api/samples/${accessnumber}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }) .then(res => res.json()).then(data => { this.item = data.data ?? {}; if (!Array.isArray(this.item.samples)) this.item.samples = []; }).finally(() => { this.isSampleLoading = false; }); }, collect(sampcode, accessnumber) { fetch(`${BASEURL}/api/samples/collect/${accessnumber}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ samplenumber: sampcode, userid: '' }) }) .then(res => res.json()).then(data => { this.fetchItem(accessnumber); }); }, unreceive(sampcode, accessnumber) { if (!confirm(`Unreceive sample ${sampcode} from request ${accessnumber}?`)) { return; } fetch(`${BASEURL}/api/samples/unreceive/${accessnumber}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ samplenumber: sampcode, userid: '' }) }) .then(res => res.json()).then(data => { this.fetchItem(accessnumber); }); }, /* preview dialog */ isDialogPreviewOpen: false, reviewed: false, previewItem: null, previewAccessnumber: null, previewType: 'preview', openPreviewDialog(accessnumber, type, item) { this.previewAccessnumber = accessnumber; this.previewItem = item; this.previewType = type; this.isDialogPreviewOpen = true; this.reviewed = false; }, closePreviewDialog() { this.isDialogPreviewOpen = false; this.previewItem = null; }, setPreviewType(type) { this.previewType = type; }, getPreviewUrl() { let base = 'http://glenlis/spooler_db/main_dev.php'; let url = `${base}?acc=${this.previewAccessnumber}`; if (this.previewType === 'ind') url += '&lang=ID'; if (this.previewType === 'eng') url += '&lang=EN'; if (this.previewType === 'pdf') url += '&output=pdf'; return url; }, validate(accessnumber, userid) { fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userid: `${userid}` }) }).then(response => { this.closePreviewDialog(); this.fetchList(); console.log('Validate clicked for', this.previewAccessnumber, 'by user', userid); }); }, /* unvalidate dialog */ isDialogUnvalOpen: false, unvalReason: '', unvalAccessnumber: null, openUnvalDialog(accessnumber) { this.unvalReason = ''; this.isDialogUnvalOpen = true; this.unvalAccessnumber = accessnumber; }, unvalidate(accessnumber, userid) { if (!confirm(`Unvalidate request ${accessnumber}?`)) { return; } fetch(`${BASEURL}/api/requests/validate/${accessnumber}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userid: `${userid}`, comment: this.unvalReason.trim() }) }).then(response => { this.closeUnvalDialog(); this.fetchList(); console.log(`Unvalidate clicked for ${accessnumber}, by user ${userid}`); }); }, closeUnvalDialog() { this.isDialogUnvalOpen = false; }, /* audit dialog */ isDialogAuditOpen: false, auditData: null, auditAccessnumber: null, auditTab: 'all', openAuditDialog(accessnumber) { this.auditAccessnumber = accessnumber; this.auditData = null; this.auditTab = 'all'; this.isDialogAuditOpen = true; this.fetchAudit(accessnumber); }, closeAuditDialog() { this.isDialogAuditOpen = false; this.auditData = null; this.auditAccessnumber = null; }, fetchAudit(accessnumber) { fetch(`${BASEURL}/api/requests/${accessnumber}/audit`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }).then(res => res.json()).then(data => { this.auditData = data.data; }); }, get getAllAuditEvents() { if (!this.auditData) return []; let events = []; let id = 0; this.auditData.validation.forEach(v => { let desc = `Validated by ${v.user}`; if (v.type === 'UNVAL') { desc = `Unvalidated by ${v.user}`; } events.push({ id: id++, category: 'validation', type: v.type, description: desc, datetime: v.datetime, user: v.user, reason: v.reason || null }); }); this.auditData.sample_collection.forEach(s => { events.push({ id: id++, category: 'sample', type: s.action === 'COLLECTED' ? 'COLLECT' : 'UNRECEIVE', description: `Tube ${s.tubenumber}: ${s.action} by ${s.user}`, datetime: s.datetime, user: s.user, reason: null }); }); return events.sort((a, b) => { if (!a.datetime) return 1; if (!b.datetime) return -1; return new Date(a.datetime) - new Date(b.datetime); }); }, get getFilteredAuditEvents() { if (this.auditTab === 'all') return this.getAllAuditEvents; return this.getAllAuditEvents.filter(e => e.category === this.auditTab); }, })); });