PageSourceSearch

https://namtrunglog.com/wp-content/themes/vhe-logistics/assets/js/main.js?ver=1.5.0

js namtrunglog.com collected 2026-10-02 01:37:01 UTC 25,234 bytes, 531 lines download raw bytes

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.