1document.addEventListener('DOMContentLoaded', () => { 2 // 0. Fetch Dynamic Exchange Rate 3 const fetchExchangeRate = () => { 4 const apiUrl = 'https://id.namtrunglog.com/client/api/exchange_rate/'; 5 fetch(apiUrl) 6 .then(response => response.json()) 7 .then(res => { 8 if (res.status === 200 && res.data && res.data.exchange_rate) { 9 const rate = res.data.exchange_rate; 10 11 // Update hidden input 12 const rateElement = document.getElementById('exchange-rate-value'); 13 if (rateElement) { 14 rateElement.value = rate; 15 } 16 17 // Update header display 18 const rateDisplay = document.getElementById('exchange-rate-display'); 19 if (rateDisplay) { 20 rateDisplay.innerText = Number(rate).toLocaleString('vi-VN'); 21 } 22 23 // Recalculate calculator total if inputs exist 24 const cnyPriceInput = document.getElementById('cny-price'); 25 if (cnyPriceInput) { 26 cnyPriceInput.dispatchEvent(new Event('input')); 27 } 28 29 const sidebarCny = document.getElementById('sidebar-cny-price'); 30 if (sidebarCny) { 31 sidebarCny.dispatchEvent(new Event('input')); 32 } 33 } 34 }) 35 .catch(error => { 36 console.error('Error fetching exchange rate:', error); 37 }); 38 }; 39 fetchExchangeRate(); 40 41 // 1. Initialize Lucide Icons 42 if (typeof lucide !== 'undefined') { 43 lucide.createIcons(); 44 } 45 46 // 2. Mobile Drawer Navigation 47 const menuToggle = document.getElementById('mobile-menu-toggle'); 48 const mobileDrawer = document.getElementById('mobile-drawer'); 49 const openIcon = document.getElementById('open-icon'); 50 const closeIcon = document.getElementById('close-icon'); 51 52 if (menuToggle && mobileDrawer) { 53 menuToggle.addEventListener('click', () => { 54 const isHidden = mobileDrawer.classList.contains('hidden'); 55 if (isHidden) { 56 mobileDrawer.classList.remove('hidden'); 57 if (openIcon) openIcon.classList.add('hidden'); 58 if (closeIcon) closeIcon.classList.remove('hidden'); 59 } else { 60 mobileDrawer.classList.add('hidden'); 61 if (openIcon) openIcon.classList.remove('hidden'); 62 if (closeIcon) closeIcon.classList.add('hidden'); 63 } 64 }); 65 } 66 67 // 3. Custom Toast Notification System 68 window.showToast = function(message, type = 'success') { 69 const container = document.getElementById('toast-container'); 70 if (!container) return; 71 72 const toast = document.createElement('div'); 73 toast.className = `p-4 rounded-xl shadow-xl border text-xs sm:text-sm font-semibold flex items-center space-x-2.5 animate-fade-in pointer-events-auto transition-all transform translate-y-0 duration-300 ${ 74 type === 'success' ? 'bg-green-50 border-green-200 text-green-800' : 75 type === 'info' ? 'bg-blue-50 border-blue-200 text-blue-800' : 76 'bg-red-50 border-red-200 text-red-800' 77 }`; 78 79 let icon = 'info'; 80 if (type === 'success') icon = 'check-circle'; 81 if (type === 'error') icon = 'alert-triangle'; 82 83 toast.innerHTML = ` 84 <i class="w-5 h-5 shrink-0" data-lucide="${icon}"></i> 85 <span>${message}</span> 86 `; 87 88 container.appendChild(toast); 89 if (typeof lucide !== 'undefined') { 90 lucide.createIcons(); 91 } 92 93 setTimeout(() => { 94 toast.classList.add('opacity-0', 'translate-y-2'); 95 setTimeout(() => { 96 toast.remove(); 97 }, 300); 98 }, 3500); 99 }; 100 101 // 4. Product Search Implementation 102 const searchBtn = document.getElementById('search-product-btn');
103 const keywordInput = document.getElementById('taobao-keyword'); 104 105 function performProductSearch() { 106 const platformSelect = document.getElementById('search-platform'); 107 if (!platformSelect || !keywordInput) return; 108 109 const platform = platformSelect.value; 110 const keyword = keywordInput.value.trim(); 111 112 if (!keyword) { 113 showToast('Vui lòng Äiá»n tên sản phẩm hoặc ÄÆ°á»ng dẫn nguá»n hà ng cần Äặt.', 'error'); 114 return; 115 } 116 117 showToast(`Äang tìm kiếm nguá»n hà ng gá»c cho từ khóa "${keyword}" trên ${platform.toUpperCase()}...`, 'info'); 118 119 const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ? 1 : 0; 120 const apiUrl = `https://id.namtrunglog.com/client/api/taobao_search/?term=${encodeURIComponent(keyword)}&site=${encodeURIComponent(platform)}&mobile=${isMobile}`; 121 122 fetch(apiUrl) 123 .then(response => response.json()) 124 .then(res => { 125 if (res.status === 200 && res.data && res.data.url) { 126 showToast('Äã tìm thấy nguá»n hà ng! Äang chuyá»n hưá»ng...', 'success'); 127 window.open(res.data.url, '_blank', 'noopener,noreferrer'); 128 } else { 129 showToast(res.message || 'Không tìm thấy kết quả phù hợp hoặc xảy ra lá»i từ há» thá»ng.', 'error'); 130 } 131 }) 132 .catch(error => { 133 console.error('Search error:', error); 134 showToast('Lá»i kết ná»i mạng hoặc há» thá»ng tìm kiếm Äang báºn.', 'error'); 135 }); 136 } 137 138 if (searchBtn) { 139 searchBtn.addEventListener('click', performProductSearch); 140 } 141 if (keywordInput) { 142 keywordInput.addEventListener('keypress', (e) => { 143 if (e.key === 'Enter') { 144 performProductSearch(); 145 } 146 }); 147 } 148 149 // 5. Package Tracking Code Helpers 150 window.setMockCode = function(code) { 151 const trackingInput = document.getElementById('tracking-code-input'); 152 if (trackingInput) { 153 trackingInput.value = code; 154 showToast(`Äã tá»± Äá»ng nháºp mã mẫu: ${code}`, 'info'); 155 } 156 }; 157 158 window.handleCodeTracking = function() { 159 const trackingInput = document.getElementById('tracking-code-input'); 160 if (!trackingInput) return; 161 162 const code = trackingInput.value.trim().toUpperCase(); 163 if (!code) { 164 showToast('Vui lòng nháºp mã váºn ÄÆ¡n Äá» bắt Äầu kiá»m tra hà nh trình.', 'error'); 165 return; 166 } 167 168 const resultBox = document.getElementById('tracking-result-section'); 169 if (!resultBox) return; 170 171 showToast(`Äang tìm kiếm hà nh trình mã váºn ÄÆ¡n "${code}"...`, 'info'); 172 173 const apiUrl = `https://id.namtrunglog.com/client/api/order_transactions/check/?term=${encodeURIComponent(code)}`; 174 175 fetch(apiUrl) 176 .then(response => response.json()) 177 .then(res => { 178 if (res.status === 200 && res.data) { 179 const data = res.data; 180 181 // Helpers inside the function 182 const formatTrackingDate = function(dateStr) { 183 if (!dateStr) return ''; 184 185 // Äảm bảo chuá»i ngà y tháng ÄÆ°á»£c parse như UTC nếu không chứa thông tin timezone 186 if (typeof dateStr === 'string' && !dateStr.endsWith('Z') && !dateStr.match(/[+-]\d{2}:?\d{2}$/)) { 187 dateStr += 'Z'; 188 } 189 190 const date = new Date(dateStr); 191 if (isNaN(date.getTime())) return dateStr; 192 193 // Chuyá»n Äá»i sang múi giá» GMT+7 194 const gmt7Date = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Ho_Chi_Minh' })); 195 196 const pad = (num) => String(num).padStart(2, '0'); 197 const hours = pad(gmt7Date.getHours()); 198 const minutes = pad(gmt7Date.getMinutes()); 199 const day = pad(gmt7Date.getDate());
200 const month = pad(gmt7Date.getMonth() + 1); 201 const year = gmt7Date.getFullYear(); 202 return `${hours}:${minutes} - ${day}/${month}/${year}`; 203 }; 204 205 const statusMap = { 206 1: { text: "Äang váºn chuyá»n", color: "bg-black text-white border-black" }, 207 2: { text: "Äã vá» kho TQ", color: "bg-green-600 text-white border-green-600" }, 208 3: { text: "Äang chuyá»n vá» VN", color: "bg-green-600 text-white border-green-600" }, 209 4: { text: "Äã vá» kho VN", color: "bg-green-600 text-white border-green-600" }, 210 5: { text: "Äã giao hà ng", color: "bg-blue-600 text-white border-blue-600" } 211 }; 212 213 const statusStyle = statusMap[data.trang_thai] || { text: "Äang xá» lý", color: "bg-gray-50 text-slate-600 border-slate-200" }; 214 215 // Generate timeline steps from non-null dates in reverse chronological order 216 const steps = []; 217 218 if (data.xuat_kho_vn) { 219 steps.push({ 220 title: "Äã xuất bưu cục Viá»t Nam, Äang tiến hà nh giao tá»i Äá»a chá» khách hà ng", 221 time: formatTrackingDate(data.xuat_kho_vn) 222 }); 223 } 224 if (data.nhap_kho_vn) { 225 steps.push({ 226 title: "ÄÆ¡n hà ng cáºp bến bưu cục phân phát Viá»t Nam", 227 time: formatTrackingDate(data.nhap_kho_vn) 228 }); 229 } 230 if (data.xuat_kho) { 231 steps.push({ 232 title: "Xuất kho phân loại thông quan Trung Quá»c vá» Viá»t Nam", 233 time: formatTrackingDate(data.xuat_kho) 234 }); 235 } 236 if (data.nhap_kho) { 237 steps.push({ 238 title: "Nháºn kiá»n hà ng bưu gá»i tại tá»ng kho Trung Quá»c", 239 time: formatTrackingDate(data.nhap_kho) 240 }); 241 } 242 if (data.ngay_tao) { 243 steps.push({ 244 title: "Ngưá»i bán gá»i bưu kiá»n, tạo ÄÆ¡n hà ng thà nh công trên há» thá»ng", 245 time: formatTrackingDate(data.ngay_tao) 246 }); 247 } 248 249 const timelineHtml = steps.map((step, index) => { 250 const dotClass = index === 0 251 ? "bg-brand-orange ring-4 ring-orange-100" 252 : "bg-slate-300"; 253 const textClass = index === 0 254 ? "font-bold text-slate-800" 255 : "font-bold text-slate-500"; 256 return ` 257 <div class="relative flex items-start"> 258 <span class="absolute -left-[19px] top-1.5 w-3 h-3 rounded-full ${dotClass}"></span> 259 <div class="ml-4"> 260 <h5 class="${textClass}">${step.title}</h5> 261 <p class="text-[10px] text-slate-400 mt-0.5">${step.time}</p> 262 </div> 263 </div> 264 `; 265 }).join(''); 266 267 resultBox.innerHTML = ` 268 <div class="bg-white border border-slate-200 rounded-2xl p-6 shadow-md relative overflow-hidden text-slate-800"> 269 <div class="absolute top-0 left-0 right-0 h-1 bg-brand-orange"></div> 270 271 <!-- Top summary bar --> 272 <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center pb-4 border-b border-slate-100 gap-2"> 273 <div>
274 <span class="text-[10px] uppercase font-extrabold text-slate-400">Mã váºn ÄÆ¡n bưu phẩm</span> 275 <h4 class="text-base font-black text-slate-900 uppercase" id="display-track-code">${data.ma_van_don}</h4> 276 </div> 277 <span class="px-3 py-0.5 rounded-full text-[10px] font-extrabold border ${statusStyle.color} uppercase" id="display-track-status">${statusStyle.text}</span> 278 </div> 279 280 <!-- Package details grid --> 281 <div class="grid grid-cols-2 sm:grid-cols-4 gap-4 py-4 my-2 border-b border-slate-100 text-xs text-slate-500 font-semibold"> 282 <div> 283 <p class="text-[10px] text-slate-400 uppercase">Mã váºn ÄÆ¡n</p> 284 <p class="text-slate-800 font-extrabold mt-0.5">${data.ma_van_don}</p> 285 </div> 286 <div> 287 <p class="text-[10px] text-slate-400 uppercase">Trá»ng lượng</p> 288 <p class="text-slate-800 font-extrabold mt-0.5">${data.can_nang ? data.can_nang + ' kg' : 'Chưa cân'}</p> 289 </div> 290 <div> 291 <p class="text-[10px] text-slate-400 uppercase">Thá» tÃch</p> 292 <p class="text-slate-800 font-extrabold mt-0.5">${data.the_tich ? data.the_tich + ' m³' : 'N/A'}</p> 293 </div> 294 <div> 295 <p class="text-[10px] text-slate-400 uppercase">Loại ÄÆ¡n hà ng</p> 296 <p class="text-slate-800 font-extrabold mt-0.5">${data.loai_don === 0 ? 'Ký gá»i' : 'Mua há»'}</p> 297 </div> 298 </div> 299 300 <!-- Timeline bưu gá»i hà nh trình --> 301 <div class="mt-6 pl-6 relative text-xs sm:text-sm"> 302 <div class="absolute left-[11px] top-1.5 bottom-1.5 w-0.5 bg-slate-200"></div> 303 <div class="space-y-6"> 304 ${timelineHtml} 305 </div> 306 </div> 307 </div> 308 `; 309 310 resultBox.classList.remove('hidden'); 311 resultBox.scrollIntoView({ behavior: 'smooth' }); 312 showToast('Lá» trình bưu phẩm ÄÆ°á»£c kết xuất thà nh công!', 'success'); 313 } else { 314 showToast('Mã váºn ÄÆ¡n không tá»n tại hoặc chưa ÄÆ°á»£c cáºp nháºt thông tin.', 'error'); 315 } 316 }) 317 .catch(error => { 318 console.error('Tracking error:', error); 319 showToast('Lá»i kết ná»i mạng hoặc há» thá»ng tra cứu Äang báºn.', 'error'); 320 }); 321 }; 322 323 // 6. Currency Exchange Rate Calculator 324 const cnyPriceInput = document.getElementById('cny-price'); 325 const cnyShipInput = document.getElementById('cny-ship'); 326 const grandTotalVnd = document.getElementById('grand-total-vnd'); 327 328 function calculateGrandTotal() { 329 if (!cnyPriceInput || !grandTotalVnd) return; 330 const priceCNY = parseFloat(cnyPriceInput.value) || 0; 331 const shipCNY = cnyShipInput ? (parseFloat(cnyShipInput.value) || 0) : 0; 332 333 // Use customizer rate or fallback 334 const rateElement = document.getElementById('exchange-rate-value'); 335 const fxRate = rateElement ? (parseInt(rateElement.value) || 4015) : 4015; 336 337 const totalCNY = priceCNY + shipCNY; 338 const totalVND = totalCNY * fxRate; 339 340 grandTotalVnd.innerText = `${Math.round(totalVND).toLocaleString('vi-VN')} Ä`; 341 } 342 343 if (cnyPriceInput) { 344 cnyPriceInput.addEventListener('input', calculateGrandTotal); 345 if (cnyShipInput) { 346 cnyShipInput.addEventListener('input', calculateGrandTotal); 347 } 348 } 349 350 // 7. Why Choose Us Accordions 351 window.toggleWhyAccordion = function(index) { 352 const body = document.getElementById(`why-body-${index}`); 353 const arrow = document.getElementById(`why-arrow-${index}`); 354 if (!body) return; 355 356 const isHidden = body.classList.contains('hidden');
357 if (isHidden) { 358 body.classList.remove('hidden'); 359 if (arrow) arrow.classList.add('rotate-180'); 360 } else { 361 body.classList.add('hidden'); 362 if (arrow) arrow.classList.remove('rotate-180'); 363 } 364 }; 365 366 // 8. FAQ Accordions 367 window.toggleFaqAccordion = function(index) { 368 const body = document.getElementById(`faq-body-${index}`); 369 const arrow = document.getElementById(`faq-arrow-${index}`); 370 if (!body) return; 371 372 const isHidden = body.classList.contains('hidden'); 373 if (isHidden) { 374 body.classList.remove('hidden'); 375 if (arrow) arrow.classList.add('rotate-180'); 376 } else { 377 body.classList.add('hidden'); 378 if (arrow) arrow.classList.remove('rotate-180'); 379 } 380 }; 381 382 // 9. Chrome / Cá»c Cá»c extension installers 383 window.triggerExtensionMock = function(browserName) { 384 showToast(`Äang má» trang cà i Äặt tiá»n Ãch cho trình duyá»t ${browserName}...`, 'info'); 385 window.open('https://chromewebstore.google.com/detail/ngnnkaggcdmjbpalcmgppkbjccnoiikk', '_blank', 'noopener,noreferrer'); 386 }; 387 388 window.triggerPriceModal = function() { 389 showToast('Biá»u phà váºn chuyá»n chÃnh ngạch NAM TRUNG LOGISTICS dao Äá»ng từ 15.000Ä - 25.000Ä/kg tùy và o loại hà ng.', 'info'); 390 }; 391 392 // 10. Map switcher: VÄn phòng Hà Ná»i (VN) <-> Kho Bằng Tưá»ng (CN) 393 window.switchMapLocation = function(locationId) { 394 const btnVn = document.getElementById('btn-map-vn'); 395 const btnCn = document.getElementById('btn-map-cn'); 396 const mapIframe = document.getElementById('map-iframe'); 397 if (!mapIframe) return; 398 399 const MAP_VN = mapIframe.dataset.mapVn || 'https://maps.google.com/maps?q=460%20Kh%C6%B0%C6%A1ng%20%C4%90%C3%ACnh%2C%20Thanh%20Xu%C3%A2n%2C%20H%C3%A0%20N%E1%BB%99i&z=16&hl=vi&output=embed'; 400 const MAP_CN = mapIframe.dataset.mapCn || 'https://maps.google.com/maps?q=Pingxiang%2C%20Chongzuo%2C%20Guangxi%2C%20China&z=12&hl=vi&output=embed'; 401 402 const ON = "px-5 py-2.5 rounded-full text-xs font-black uppercase tracking-wider bg-white text-brand-orange shadow-sm transition-all"; 403 const OFF = "px-5 py-2.5 rounded-full text-xs font-black uppercase tracking-wider text-slate-500 transition-all"; 404 405 mapIframe.style.opacity = '0.3'; 406 407 setTimeout(() => { 408 if (locationId === 'vn') { 409 if (btnVn) btnVn.className = ON; 410 if (btnCn) btnCn.className = OFF; 411 mapIframe.src = MAP_VN; 412 showToast('VÄn phòng giao dá»ch: 460 Khương Äình, Thanh Xuân, Hà Ná»i.', 'info'); 413 } else { 414 if (btnVn) btnVn.className = OFF; 415 if (btnCn) btnCn.className = ON; 416 mapIframe.src = MAP_CN; 417 showToast('Kho Trung Quá»c: Bằng Tưá»ng, Trung Quá»c.', 'info'); 418 } 419 mapIframe.style.opacity = '1'; 420 }, 300); 421 }; 422 423 // 11. AJAX Submission for Homepage Registration Form 424 const partnerForm = document.querySelector('#contact form'); 425 if (partnerForm) { 426 partnerForm.addEventListener('submit', (e) => { 427 e.preventDefault(); 428 const name = document.getElementById('reg-name').value.trim(); 429 const phone = document.getElementById('reg-phone').value.trim(); 430 const notes = document.getElementById('reg-notes') ? document.getElementById('reg-notes').value.trim() : ''; 431 432 if (!name || !phone) { 433 showToast('Vui lòng hoà n thà nh Äầy Äá»§ thông tin bắt buá»c.', 'error'); 434 return; 435 } 436 437 // AJAX call to WordPress backend 438 const formData = new FormData(); 439 formData.append('action', 'vhe_submit_partner_form'); 440 formData.append('fullname', name); 441 formData.append('phone', phone); 442 formData.append('notes', notes); 443 if (typeof vhe_ajax_obj !== 'undefined') { 444 formData.append('security', vhe_ajax_obj.nonce); 445 } 446 447 showToast('Äang gá»i thông tin ÄÄng ký...', 'info'); 448 449 const ajaxUrl = typeof vhe_ajax_obj !== 'undefined' ? vhe_ajax_obj.ajax_url : '/wp-admin/admin-ajax.php'; 450 451 fetch(ajaxUrl, { 452 method: 'POST', 453 body: formData 454 }) 455 .then(res => res.json()) 456 .then(res => { 457 if (res.success) { 458 showToast(res.data.message || 'ÄÄng ký thà nh công! NAM TRUNG LOGISTICS sẽ liên há» sá»m.', 'success'); 459 partnerForm.reset(); 460 } else { 461 showToast(res.data.message || 'Lá»i gá»i yêu cầu!', 'error'); 462 } 463 }) 464 .catch(err => { 465 console.error(err); 466 showToast('Lá»i há» thá»ng hoặc lá»i kết ná»i mạng!', 'error'); 467 }); 468 }); 469 } 470 471 // 12. AJAX Submission for Contact Page Form 472 const contactUsForm = document.querySelector('.lg\\:col-span-7 form'); 473 if (contactUsForm) { 474 contactUsForm.addEventListener('submit', (e) => { 475 e.preventDefault(); 476 const name = document.getElementById('contact-name').value.trim(); 477 const phone = document.getElementById('contact-phone').value.trim(); 478 const email = document.getElementById('contact-email').value.trim(); 479 const subject = document.getElementById('contact-subject').value; 480 const message = document.getElementById('contact-message').value.trim(); 481 482 if (!name || !phone || !email || !message) { 483 showToast('Vui lòng hoà n thà nh Äầy Äá»§ tất cả các trưá»ng thông tin.', 'error'); 484 return; 485 } 486 487 const formData = new FormData(); 488 formData.append('action', 'vhe_submit_contact_form'); 489 formData.append('fullname', name); 490 formData.append('phone', phone); 491 formData.append('email', email); 492 formData.append('subject', subject); 493 formData.append('message', message); 494 if (typeof vhe_ajax_obj !== 'undefined') { 495 formData.append('security', vhe_ajax_obj.nonce); 496 } 497 498 showToast('Äang gá»i yêu cầu há» trợ tá»i bưu cục...', 'info'); 499 500 const ajaxUrl = typeof vhe_ajax_obj !== 'undefined' ? vhe_ajax_obj.ajax_url : '/wp-admin/admin-ajax.php'; 501 502 fetch(ajaxUrl, { 503 method: 'POST', 504 body: formData 505 }) 506 .then(res => res.json()) 507 .then(res => { 508 if (res.success) { 509 const mockBox = document.getElementById('mock-inquiry-box'); 510 const inquiryUser = document.getElementById('inquiry-user'); 511 const inquiryId = document.getElementById('inquiry-id'); 512 513 if (mockBox && inquiryUser && inquiryId) { 514 inquiryUser.innerText = `${name} (${phone})`; 515 inquiryId.innerText = res.data.inquiry_id || ('NT-' + Math.floor(100 + Math.random() * 900)); 516 mockBox.classList.remove('hidden'); 517 } 518 519 showToast(res.data.message || 'Yêu cầu há» trợ Äã ÄÆ°á»£c gá»i thà nh công!', 'success'); 520 contactUsForm.reset(); 521 } else { 522 showToast(res.data.message || 'Lá»i há» thá»ng khi gá»i yêu cầu!', 'error'); 523 } 524 }) 525 .catch(err => { 526 console.error(err); 527 showToast('Lá»i há» thá»ng hoặc lá»i kết ná»i mạng!', 'error'); 528 }); 529 }); 530 } 531});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.