const UNSELECTED_SVG = ` `; const SELECTED_SVG = ` `; class TrackAndTraceWidget { constructor(baseUrl, mountSelector) { this.baseUrl = baseUrl; this.root = document.querySelector(mountSelector); this.data = []; this.activeIndex = 0; this.signatureModal = null; this.signatureImageEl = null; this.signatureMetaEl = null; this.currentSignatureUrl = null; } init() { this.renderSkeleton(); this.bindSearch(); } renderSkeleton() { this.root.innerHTML = `
Digital Signature
Digital signature preview
`; this.setupSignatureModal(); } bindSearch() { const input = this.root.querySelector('input'); const button = this.root.querySelector('button'); button.onclick = () => this.fetchData(input.value.trim()); } async fetchData(search) { if (!search) return; try { const res = await fetch( `${this.baseUrl}/MVC/CourierStatus/GetStatusWithDetails?search=${encodeURIComponent(search)}` ); if (!res.ok) { throw new Error('API request failed'); } const json = await res.json(); if (!Array.isArray(json) || json.length === 0) { console.warn('No tracking data found'); return; } this.data = json; this.activeIndex = 0; // đŸ”Ĩ SHOW LAYOUT ONLY AFTER DATA IS VALID const layout = this.root.querySelector('#tt-layout'); layout.style.display = 'flex'; this.render(); } catch (err) { console.error('Track & Trace error:', err); } } render() { this.renderMaster(); this.renderSwitcher(); this.renderTimeline(); } renderMaster() { const el = this.root.querySelector('#tt-master'); const d = this.data[this.activeIndex]; if (!d) { el.innerHTML = ''; return; } const totalEvents = Array.isArray(d.Details) ? d.Details.length : 0; el.innerHTML = `
Tracking Summary
${this.row('Job No', d.JobNumber)} ${this.row('Booking No', d.BookingNumber)} ${this.row('Place of Receipt', d.Origin)} ${this.row('Place of Delivery', d.Destination)} ${this.row('Vessel / Voyage', d.VesselName)} ${this.row('Complete Loading', d.CompleteLoading)} ${this.row('Complete Discharge', d.CompleteDischarge)} `; } row(label, value) { return `
${this.iconForLabel(label)} ${label}
${value || '-'}
`; } iconForLabel(label) { const map = { 'Job No': '📄', 'Booking No': 'đŸŽŸī¸', 'Place of Receipt': '📍', 'Place of Delivery': '📍', 'Vessel / Voyage': 'â›´ī¸', 'Complete Loading': 'đŸšĸ', 'Complete Discharge': '⚓', }; return map[label] || 'â„šī¸'; } renderSwitcher() { const el = this.root.querySelector('#tt-switcher'); el.innerHTML = ''; this.data.forEach((c, i) => { const btn = document.createElement('div'); btn.className = 'tt-truck-btn'; const svg = (i === this.activeIndex) ? SELECTED_SVG : UNSELECTED_SVG; btn.innerHTML = `
${svg} ${c.ContainerId}
`; btn.onclick = () => { this.activeIndex = i; this.render(); }; el.appendChild(btn); }); } renderTimeline() { const el = this.root.querySelector('#tt-timeline'); const container = this.data[this.activeIndex]; if (!container || !Array.isArray(container.Details)) { el.innerHTML = ''; return; } const details = [...container.Details].sort( (a, b) => new Date(b.Date) - new Date(a.Date) ); el.innerHTML = details.map(d => { const dateParts = this.formatDateParts(d.Date); const trimmedStatus = (d.Status || '').trim(); const normalizedStatus = trimmedStatus.toLowerCase(); const colorClass = this.statusColorClass(normalizedStatus); const saleId = this.extractSaleId(d) || this.extractSaleId(container); const shouldShowAction = this.shouldShowSignatureButton(normalizedStatus); const saleAttr = saleId ? `data-sale-id="${saleId}"` : ''; const actionButton = shouldShowAction ? `` : ''; return `
${dateParts.day}
${dateParts.month}
${dateParts.year}
${d.Status || '—'}
${d.Location || ''}
${d.Time || ''} ${actionButton}
${dateParts.fullDate}${d.Time ? ' â€ĸ ' + d.Time : ''}
`; }).join(''); this.attachSignatureHandlers(); } attachSignatureHandlers() { const actions = this.root.querySelectorAll('.tt-event-action'); actions.forEach(btn => { const saleId = btn.dataset.saleId; const eventTitle = btn.dataset.eventTitle; btn.onclick = () => this.fetchAndShowSignature(saleId, eventTitle); }); } setupSignatureModal() { this.signatureModal = this.root.querySelector('#tt-signature-modal'); if (!this.signatureModal) return; const backdrop = this.signatureModal.querySelector('[data-role="backdrop"]'); const closeButton = this.signatureModal.querySelector('.tt-modal-close'); this.signatureImageEl = this.signatureModal.querySelector('.tt-signature-img'); this.signatureMetaEl = this.signatureModal.querySelector('[data-role="meta"]'); const closeHandler = () => this.closeSignatureModal(); backdrop?.addEventListener('click', closeHandler); closeButton?.addEventListener('click', closeHandler); } shouldShowSignatureButton(normalizedStatus) { return normalizedStatus === 'deliver to customer'; } extractSaleId(detail) { return detail?.SaleId || detail?.saleId || detail?.SaleID || detail?.saleID || null; } escapeHtml(value) { return (value || '').replace(/[&<>"']/g, char => { const escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return escapeMap[char]; }); } async fetchAndShowSignature(saleId, title) { if (!saleId) return; try { const payload = await this.fetchJson(`${this.getApiUrl('api/digitalsignature/get-data')}?SaleId=${saleId}`); const attachments = Array.isArray(payload?.attachments) ? payload.attachments : []; const attachment = attachments.find(item => this.getAttachmentArchiveId(item)); if (!attachment) { console.warn('No digital signature attachment available'); return; } const archiveId = this.getAttachmentArchiveId(attachment); if (!archiveId) return; const downloadUrl = `${this.getApiUrl('MVC/Archive/DownloadArchive')}?ArchiveId=${archiveId}`; const imageBlob = await this.fetchBlob(downloadUrl); const objectUrl = URL.createObjectURL(imageBlob); if (this.currentSignatureUrl) { URL.revokeObjectURL(this.currentSignatureUrl); } this.currentSignatureUrl = objectUrl; const metaText = `${title || attachment?.title || 'DigitalSignature'} ${attachment?.date || ''} ${attachment?.time || ''}`.trim(); this.openSignatureModal(objectUrl, metaText); } catch (err) { console.error('Digital signature error', err); } } openSignatureModal(imageUrl, metaText) { if (!this.signatureModal || !this.signatureImageEl) return; this.signatureImageEl.src = imageUrl; if (this.signatureMetaEl) { this.signatureMetaEl.textContent = metaText; } this.signatureModal.classList.add('is-open'); } closeSignatureModal() { if (!this.signatureModal) return; this.signatureModal.classList.remove('is-open'); if (this.signatureImageEl) { this.signatureImageEl.removeAttribute('src'); } if (this.currentSignatureUrl) { URL.revokeObjectURL(this.currentSignatureUrl); this.currentSignatureUrl = null; } } getAttachmentArchiveId(attachment) { return attachment?.archiveId ?? attachment?.ArchiveId ?? null; } getApiUrl(path) { const normalizedBase = this.baseUrl.replace(/\/+$/, ''); return `${normalizedBase}/${path.replace(/^\/+/, '')}`; } async fetchJson(url) { const res = await fetch(url); if (!res.ok) { throw new Error('Digital signature request failed'); } return res.json(); } async fetchBlob(url) { const res = await fetch(url); if (!res.ok) { throw new Error('Archive download failed'); } return res.blob(); } formatDateParts(dateString) { if (!dateString) { return { day: '--', month: '', year: '', fullDate: '' }; } const date = new Date(dateString); if (Number.isNaN(date.getTime())) { return { day: '--', month: '', year: '', fullDate: dateString }; } const day = String(date.getDate()).padStart(2, '0'); const month = date.toLocaleString('en-US', { month: 'short' }).toUpperCase(); const year = date.getFullYear(); const fullDate = `${month} ${day}, ${year}`; return { day, month, year, fullDate }; } statusColorClass(status) { const map = { 'deliver to customer': 'status-primary', 'arrived at hub': 'status-success', 'departed hub': 'status-warning', 'pending': 'status-warning', 'delivered': 'status-success', }; return map[status] || 'status-default'; } }