commit 6216ff39b4988f16a1c7a0e07ca208b1615f18f7 Author: sina Date: Wed Jan 28 14:47:47 2026 +0330 initialize diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..f0ea038 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + Track & Trace + + + + +
+ + + + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..d02c799 --- /dev/null +++ b/styles.css @@ -0,0 +1,501 @@ +:root { + --primary: #2f6df6; + --bg: #f4f7fb; + --card-bg: #ffffff; + --border: #e6ecf5; + --text-main: #1f2937; + --text-muted: #6b7280; + --success: #4ade80; + --warning: #facc15; + --danger: #f87171; + --shadow-soft: 0 10px 30px rgba(0,0,0,0.06); + } + + * { + box-sizing: border-box; + font-family: Inter, system-ui, sans-serif; + } + + body { + margin: 0; + background: var(--bg); + } + + .tt-widget { + max-width: 1200px; + margin: auto; + padding: 16px; + } + + .tt-search { + display: flex; + gap: 12px; + margin-bottom: 20px; + } + + .tt-search input { + flex: 1; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid var(--border); + font-size: 15px; + } + + .tt-search button { + padding: 14px 20px; + border-radius: 14px; + border: none; + background: var(--primary); + color: white; + font-weight: 600; + cursor: pointer; + } + + .tt-layout { + display: flex; + gap: 20px; + } + + .tt-master-card { + background: var(--card-bg); + border-radius: 18px; + padding: 24px; + width: 100%; + max-width: 420px; + box-shadow: var(--shadow-soft); + } + + .tt-master-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 14px; + } + + .tt-master-title { + font-weight: 700; + font-size: 20px; + } + + .tt-event-count-btn { + border: none; + background: #eef2ff; + color: var(--primary); + font-weight: 600; + font-size: 13px; + border-radius: 999px; + padding: 6px 16px; + cursor: pointer; + box-shadow: inset 0 0 0 1px rgba(47, 109, 246, 0.25); + } + + .tt-master-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid var(--border); + font-size: 14px; + } + + .tt-master-row:last-child { + border-bottom: none; + padding-bottom: 0; + } + + .tt-master-row strong { + font-size: 15px; + color: var(--text-main); + } + + .tt-master-row-label { + display: flex; + align-items: center; + gap: 12px; + } + + .tt-master-icon { + width: 36px; + height: 36px; + border-radius: 12px; + background: #eff4ff; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + } + + .tt-timeline-area { + flex: 1; + background: #ebefff; + border-radius: 18px; + padding: 24px 28px; + box-shadow: var(--shadow-soft); + } + + .tt-container-switcher { + display: flex; + gap: 10px; + margin-bottom: 20px; + overflow-x: auto; + } + + .tt-container-btn { + padding: 8px 14px; + border-radius: 20px; + border: 1px solid var(--border); + background: #f1f5ff; + color: var(--primary); + cursor: pointer; + font-size: 13px; + white-space: nowrap; + } + + .tt-container-btn.active { + background: var(--primary); + color: #fff; + } + + .tt-timeline { + position: relative; + padding-left: 60px; + background: #f4f6ff; + border-radius: 16px; + padding-top: 16px; + padding-bottom: 16px; + } + + .tt-timeline::before { + content: ''; + position: absolute; + left: 191px; + top: 12px; + bottom: 12px; + width: 3px; + background: linear-gradient(180deg, rgba(47, 109, 246, 0.35), rgba(47, 109, 246, 0.1)); + } + + .tt-timeline-row { + position: relative; + display: grid; + grid-template-columns: 96px 40px 1fr; + gap: 16px; + margin-bottom: 18px; + align-items: center; + } + + .tt-timeline-row:last-child { + margin-bottom: 0; + } + + .tt-timeline-date { + text-align: right; + font-weight: 600; + color: var(--text-muted); + padding-right: 6px; + } + + .tt-timeline-line { + position: relative; + width: 40px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + } + + .tt-timeline-line span { + width: 20px; + height: 20px; + border-radius: 50%; + border: 4px solid rgba(101, 105, 186, 0.4); + background: #f4f6ff; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.4); + transition: box-shadow 0.2s ease, border-color 0.2s ease; + position: relative; + z-index: 1; + } + + .tt-timeline-row.status-primary .tt-timeline-line span { + border-color: rgba(47, 109, 246, 0.8); + box-shadow: 0 0 0 3px rgba(47, 109, 246, 0.12); + } + + .tt-timeline-row.status-success .tt-timeline-line span { + border-color: rgba(34, 197, 94, 0.8); + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12); + } + + .tt-timeline-row.status-warning .tt-timeline-line span { + border-color: rgba(245, 158, 11, 0.8); + box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.12); + } + + .tt-timeline-row.status-default .tt-timeline-line span { + border-color: rgba(160, 174, 192, 0.8); + box-shadow: 0 0 0 3px rgba(160, 174, 192, 0.12); + } + + .tt-signature-modal { + position: fixed; + inset: 0; + display: none; + align-items: center; + justify-content: center; + z-index: 999; + } + + .tt-signature-modal.is-open { + display: flex; + } + + .tt-signature-backdrop { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.55); + backdrop-filter: blur(4px); + } + + .tt-signature-card { + position: relative; + background: #0f172a; + border-radius: 20px; + padding: 24px; + width: min(640px, 95%); + z-index: 1; + box-shadow: 0 25px 60px rgba(15, 23, 42, 0.45); + } + + .tt-signature-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 18px; + color: #fff; + } + + .tt-signature-title { + font-weight: 600; + font-size: 18px; + } + + .tt-modal-close { + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + background: rgba(255, 255, 255, 0.2); + color: #fff; + font-size: 24px; + cursor: pointer; + line-height: 1; + } + + .tt-signature-body { + background: #fff; + border-radius: 18px; + padding: 16px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.04); + } + + .tt-signature-img { + width: 100%; + border-radius: 12px; + height: 320px; + object-fit: contain; + background: #f9fafb; + display: block; + } + + .tt-signature-meta { + margin-top: 12px; + font-size: 13px; + color: #0f172a; + text-align: center; + } + + .tt-timeline-day { + font-size: 24px; + color: var(--text-main); + font-weight: 700; + } + + .tt-timeline-month, + .tt-timeline-year { + font-size: 12px; + letter-spacing: 0.3px; + } + + .tt-timeline-card { + flex: 1; + background: #ffffff; + border-radius: 22px; + padding: 18px 22px; + box-shadow: 0 20px 60px rgba(63, 83, 180, 0.16); + border: none; + display: flex; + flex-direction: column; + gap: 8px; + } + + .tt-timeline-card-header { + display: grid; + grid-template-columns: auto 1fr auto auto; + gap: 12px; + align-items: center; + } + + .tt-card-text { + display: flex; + flex-direction: column; + gap: 2px; + } + + .tt-event-title { + font-weight: 600; + font-size: 15px; + color: var(--text-main); + } + + .tt-event-location { + font-size: 13px; + color: var(--text-muted); + } + + .tt-event-time { + font-size: 13px; + font-weight: 600; + color: var(--primary); + } + + .tt-event-action { + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: #eef2ff; + color: var(--primary); + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease, opacity 0.2s ease; + } + + .tt-event-action:hover { + transform: scale(1.05); + } + + .tt-event-action:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .tt-event-meta { + font-size: 12px; + color: var(--text-muted); + } + + .tt-event-time { + padding: 6px 10px; + border-radius: 999px; + font-size: 13px; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(47, 109, 246, 0.12); + color: #2f6df6; + } + + .tt-event-time.status-primary { + background: rgba(47, 109, 246, 0.12); + color: #1d4ed8; + } + + .tt-event-time.status-success { + background: rgba(34, 197, 94, 0.16); + color: #059669; + } + + .tt-event-time.status-warning { + background: rgba(245, 158, 11, 0.16); + color: #b45309; + } + + .tt-event-time.status-default { + background: rgba(148, 163, 184, 0.16); + color: #475569; + } + + .tt-status-dot { + width: 12px; + height: 12px; + border-radius: 50%; + border: 3px solid #fff; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); + } + + .tt-status-dot.status-primary { + background: #2f6df6; + } + + .tt-status-dot.status-success { + background: #22c55e; + } + + .tt-status-dot.status-warning { + background: #f59e0b; + } + + .tt-status-dot.status-default { + background: #a3a3a3; + } + +.tt-container-switcher { + display: flex; + gap: 12px; + margin-bottom: 20px; + overflow-x: auto; + } + + .tt-truck-btn { + position: relative; + cursor: pointer; + flex-shrink: 0; + } + + .tt-truck-svg { + position: relative; + width: 86px; + height: 36px; + } + + .tt-truck-svg svg { + width: 100%; + height: 100%; + display: block; + } + + .tt-truck-label { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #0a2540; + pointer-events: none; + } + @media (max-width: 768px) { + .tt-layout { + flex-direction: column; + } + + .tt-master-card { + max-width: 100%; + } + } + \ No newline at end of file diff --git a/tracker.js b/tracker.js new file mode 100644 index 0000000..78a112f --- /dev/null +++ b/tracker.js @@ -0,0 +1,396 @@ +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'; + } + + } + \ No newline at end of file