Skip to content
/** * CÔNG CỤ LỊCH MỌC RĂNG SỮA CHUYÊN NGHIỆP * Phát triển bởi DrNinhMai.com * Dựa trên nghiên cứu khoa học từ NCBI PMC, Cleveland Clinic * Version: 2.0 - Professional Grade */ // =================== DỮ LIỆU KHOA HỌC =================== // Dữ liệu mọc răng dựa trên nghiên cứu NCBI PMC - Ogodescu et al. (2022) const toothEruptionData = { primary: { 'lower_central_incisors': { min: 5, max: 9, average: 7, position: 'Răng cửa giữa hàm dưới', code: '71, 81' }, 'upper_central_incisors': { min: 8, max: 12, average: 10, position: 'Răng cửa giữa hàm trên', code: '51, 61' }, 'upper_lateral_incisors': { min: 10, max: 12, average: 11, position: 'Răng cửa bên hàm trên', code: '52, 62' }, 'lower_lateral_incisors': { min: 12, max: 15, average: 13, position: 'Răng cửa bên hàm dưới', code: '72, 82' }, 'upper_first_molars': { min: 12, max: 16, average: 14, position: 'Răng hàm nhỏ thứ nhất hàm trên', code: '54, 64' }, 'lower_first_molars': { min: 12, max: 16, average: 14, position: 'Răng hàm nhỏ thứ nhất hàm dưới', code: '74, 84' }, 'upper_canines': { min: 16, max: 20, average: 18, position: 'Răng nanh hàm trên', code: '53, 63' }, 'lower_canines': { min: 16, max: 20, average: 18, position: 'Răng nanh hàm dưới', code: '73, 83' }, 'upper_second_molars': { min: 20, max: 30, average: 24, position: 'Răng hàm nhỏ thứ hai hàm trên', code: '55, 65' }, 'lower_second_molars': { min: 20, max: 30, average: 24, position: 'Răng hàm nhỏ thứ hai hàm dưới', code: '75, 85' } } }; // BMI Percentiles cho trẻ em (WHO Growth Standards) const bmiPercentiles = { male: { 6: { p3: 13.6, p15: 14.4, p50: 15.8, p85: 17.4, p97: 19.3 }, 12: { p3: 14.1, p15: 15.0, p50: 16.4, p85: 18.1, p97: 20.2 }, 18: { p3: 14.4, p15: 15.3, p50: 16.7, p85: 18.4, p97: 20.6 }, 24: { p3: 14.7, p15: 15.6, p50: 17.0, p85: 18.8, p97: 21.1 }, 36: { p3: 15.2, p15: 16.1, p50: 17.6, p85: 19.6, p97: 22.3 } }, female: { 6: { p3: 13.2, p15: 14.0, p50: 15.3, p85: 16.9, p97: 18.8 }, 12: { p3: 13.6, p15: 14.4, p50: 15.8, p85: 17.5, p97: 19.6 }, 18: { p3: 13.9, p15: 14.7, p50: 16.2, p85: 18.0, p97: 20.2 }, 24: { p3: 14.2, p15: 15.0, p50: 16.5, p85: 18.4, p97: 20.8 }, 36: { p3: 14.8, p15: 15.7, p50: 17.2, p85: 19.4, p97: 22.1 } } }; // Khuyến cáo dinh dưỡng dựa trên nghiên cứu khoa học const nutritionRecommendations = { breastfeeding: { factor: -0.5, description: 'Bú mẹ hoàn toàn giúp răng mọc sớm hơn và khỏe mạnh hơn', source: 'Journal of Pediatric Dentistry (2021)' }, mixed: { factor: 0, description: 'Kết hợp bú mẹ và sữa công thức cân bằng', source: 'WHO Infant Feeding Guidelines' }, formula: { factor: 1, description: 'Sữa công thức có thể làm chậm quá trình mọc răng', source: 'Clinical Pediatric Dentistry Research (2020)' } }; // =================== CLASS CHÍNH =================== class ToothEruptionCalculator { constructor() { this.chartInstance = null; this.currentData = null; this.initializeEventListeners(); this.addModalStyles(); this.hideLoading(); this.initializeTooltips(); } // =================== KHỞI TẠO =================== initializeEventListeners() { // Form submission document.getElementById('toothForm').addEventListener('submit', (e) => { e.preventDefault(); this.calculateEruption(); }); // Reset button document.getElementById('resetBtn').addEventListener('click', () => { this.resetForm(); }); // Export buttons document.getElementById('exportPDF').addEventListener('click', () => { this.exportToPDF(); }); document.getElementById('emailReport').addEventListener('click', () => { this.showEmailDialog(); }); document.getElementById('printReport').addEventListener('click', () => { this.printReport(); }); // Real-time validation this.initializeRealTimeValidation(); // Keyboard shortcuts this.initializeKeyboardShortcuts(); } initializeRealTimeValidation() { const inputs = document.querySelectorAll('input, select'); inputs.forEach(input => { input.addEventListener('input', () => { this.validateField(input); }); input.addEventListener('blur', () => { this.validateField(input); }); }); } initializeKeyboardShortcuts() { document.addEventListener('keydown', (e) => { // Ctrl/Cmd + Enter để submit if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); document.getElementById('toothForm').dispatchEvent(new Event('submit')); } // Escape để đóng modal if (e.key === 'Escape') { const modal = document.querySelector('.modal-overlay'); if (modal) { document.body.removeChild(modal); } } }); } initializeTooltips() { // Thêm tooltip cho các trường input const tooltips = { 'childName': 'Nhập tên đầy đủ của trẻ để tạo báo cáo cá nhân hóa', 'birthDate': 'Ngày sinh chính xác để tính toán tuổi theo tháng', 'gender': 'Giới tính ảnh hưởng đến thời gian mọc răng', 'weight': 'Cân nặng hiện tại (kg) để tính BMI', 'height': 'Chiều cao hiện tại (cm) để tính BMI', 'feedingType': 'Hình thức nuôi dưỡng ảnh hưởng đến phát triển răng' }; Object.entries(tooltips).forEach(([id, tooltip]) => { const element = document.getElementById(id); if (element) { element.setAttribute('title', tooltip); element.setAttribute('data-tooltip', tooltip); } }); } // =================== VALIDATION =================== validateField(field) { const value = field.value.trim(); const fieldName = field.getAttribute('name') || field.id; let isValid = true; let errorMessage = ''; // Reset previous validation field.classList.remove('error', 'success'); this.removeErrorMessage(field); // Validate based on field type switch (fieldName) { case 'childName': if (value.length < 2) { isValid = false; errorMessage = 'Tên trẻ phải có ít nhất 2 ký tự'; } break; case 'birthDate': if (value) { const birthDate = new Date(value); const today = new Date(); const age = this.calculateAge(value); if (birthDate > today) { isValid = false; errorMessage = 'Ngày sinh không thể sau ngày hiện tại'; } else if (age.years > 3) { isValid = false; errorMessage = 'Công cụ chỉ áp dụng cho trẻ dưới 3 tuổi'; } } break; case 'weight': if (value) { const weight = parseFloat(value); if (weight < 2 || weight > 50) { isValid = false; errorMessage = 'Cân nặng phải trong khoảng 2-50kg'; } } break; case 'height': if (value) { const height = parseFloat(value); if (height < 40 || height > 150) { isValid = false; errorMessage = 'Chiều cao phải trong khoảng 40-150cm'; } } break; } // Apply validation styling if (field.hasAttribute('required') && !value) { field.classList.add('error'); } else if (isValid && value) { field.classList.add('success'); } else if (!isValid) { field.classList.add('error'); this.showErrorMessage(field, errorMessage); } return isValid; } showErrorMessage(field, message) { const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.textContent = message; field.parentNode.appendChild(errorDiv); } removeErrorMessage(field) { const errorMsg = field.parentNode.querySelector('.error-message'); if (errorMsg) { errorMsg.remove(); } } // =================== TÍNH TOÁN CHÍNH =================== calculateAge(birthDate) { const today = new Date(); const birth = new Date(birthDate); const diffTime = Math.abs(today - birth); const ageMonths = Math.floor(diffTime / (1000 * 60 * 60 * 24 * 30.44)); const ageYears = Math.floor(ageMonths / 12); const remainingMonths = ageMonths % 12; const ageDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); return { months: ageMonths, years: ageYears, remainingMonths, days: ageDays, exact: ageMonths + (ageDays % 30) / 30 }; } calculateBMI(weight, height, ageMonths, gender) { if (!weight || !height) return null; const bmi = (weight / ((height / 100) ** 2)); // Tìm khoảng tuổi gần nhất trong bảng percentile const ages = Object.keys(bmiPercentiles[gender]).map(Number); const closestAge = ages.reduce((prev, curr) => { return Math.abs(curr - ageMonths) < Math.abs(prev - ageMonths) ? curr : prev; }); const percentiles = bmiPercentiles[gender][closestAge]; let category, status, color, percentile; if (bmi < percentiles.p3) { category = 'Suy dinh dưỡng nặng'; status = 'danger'; color = '#ef4444'; percentile = '<3'; } else if (bmi < percentiles.p15) { category = 'Suy dinh dưỡng nhẹ'; status = 'warning'; color = '#f59e0b'; percentile = '3-15'; } else if (bmi < percentiles.p85) { category = 'Bình thường'; status = 'normal'; color = '#10b981'; percentile = '15-85'; } else if (bmi < percentiles.p97) { category = 'Thừa cân'; status = 'warning'; color = '#f59e0b'; percentile = '85-97'; } else { category = 'Béo phì'; status = 'danger'; color = '#ef4444'; percentile = '>97'; } return { bmi: bmi.toFixed(1), category, status, color, percentiles, percentile, zScore: this.calculateBMIZScore(bmi, ageMonths, gender) }; } calculateBMIZScore(bmi, ageMonths, gender) { // Simplified Z-score calculation const percentiles = bmiPercentiles[gender][Math.min(...Object.keys(bmiPercentiles[gender]).map(Number).filter(age => age <= ageMonths)) || 6]; const median = percentiles.p50; const sd = (percentiles.p85 - percentiles.p50) / 1.04; // Approximation return ((bmi - median) / sd).toFixed(2); } getNutritionFactor(bmiResult, feedingType, ageMonths) { let factor = 0; // BMI factor if (bmiResult) { switch (bmiResult.status) { case 'danger': factor += bmiResult.category.includes('Suy dinh dưỡng') ? 3 : -2; break; case 'warning': factor += bmiResult.category.includes('Suy dinh dưỡng') ? 2 : -1; break; case 'normal': factor += 0; break; } } // Feeding type factor const feedingData = nutritionRecommendations[feedingType]; if (feedingData) { factor += feedingData.factor; } // Age adjustment (premature vs mature) if (ageMonths < 6) { factor += 1; // Trẻ quá nhỏ có thể chậm hơn } return Math.max(-3, Math.min(6, factor)); // Giới hạn factor } getEruptionStatus(toothData, ageMonths) { const tolerance = 2; // 2 tháng tolerance if (ageMonths < toothData.min - tolerance) { return { status: 'not-erupted', text: 'Chưa đến thời gian', class: 'tooth-not-erupted', urgency: 'none' }; } else if (ageMonths < toothData.min) { return { status: 'pre-erupting', text: 'Sắp mọc', class: 'tooth-pre-erupting', urgency: 'low' }; } else if (ageMonths <= toothData.max) { return { status: 'erupting', text: 'Đang mọc', class: 'tooth-erupting', urgency: 'normal' }; } else if (ageMonths <= toothData.max + 3) { return { status: 'erupted', text: 'Đã mọc', class: 'tooth-erupted', urgency: 'none' }; } else if (ageMonths <= toothData.max + 6) { return { status: 'late', text: 'Mọc muộn', class: 'tooth-late', urgency: 'medium' }; } else { return { status: 'overdue', text: 'Trễ hẹn', class: 'tooth-overdue', urgency: 'high' }; } } // =================== HIỂN THỊ KẾT QUẢ =================== showLoading() { document.getElementById('loadingOverlay').style.display = 'flex'; } hideLoading() { document.getElementById('loadingOverlay').style.display = 'none'; } async calculateEruption() { // Validate form trước khi tính toán if (!this.validateForm()) { return; } this.showLoading(); try { // Lấy dữ liệu từ form const formData = { childName: document.getElementById('childName').value, birthDate: document.getElementById('birthDate').value, gender: document.getElementById('gender').value, weight: parseFloat(document.getElementById('weight').value), height: parseFloat(document.getElementById('height').value), feedingType: document.getElementById('feedingType').value }; // Tính tuổi const age = this.calculateAge(formData.birthDate); // Tính BMI const bmiResult = this.calculateBMI(formData.weight, formData.height, age.months, formData.gender); // Điều chỉnh thời gian mọc răng const nutritionFactor = this.getNutritionFactor(bmiResult, formData.feedingType, age.months); // Lưu dữ liệu hiện tại this.currentData = { formData, age, bmiResult, nutritionFactor }; // Hiển thị kết quả với animation await this.displayResults(this.currentData); } catch (error) { console.error('Lỗi tính toán:', error); this.showNotification('Có lỗi xảy ra khi tính toán. Vui lòng thử lại.', 'error'); } finally { this.hideLoading(); } } validateForm() { const requiredFields = ['childName', 'birthDate', 'gender']; let isValid = true; requiredFields.forEach(fieldId => { const field = document.getElementById(fieldId); if (!this.validateField(field)) { isValid = false; } }); if (!isValid) { this.showNotification('Vui lòng điền đầy đủ thông tin bắt buộc', 'warning'); } return isValid; } async displayResults(data) { const { formData, age, bmiResult, nutritionFactor } = data; // Hiển thị section kết quả với animation const resultsSection = document.getElementById('resultsSection'); resultsSection.style.display = 'block'; resultsSection.style.opacity = '0'; resultsSection.style.transform = 'translateY(30px)'; // Smooth scroll resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Fade in animation setTimeout(() => { resultsSection.style.transition = 'all 0.6s ease-out'; resultsSection.style.opacity = '1'; resultsSection.style.transform = 'translateY(0)'; }, 100); // Hiển thị từng phần với delay await this.animateResultSections([ () => this.displayBMIResult(bmiResult), () => this.createTimelineChart(age.months, nutritionFactor, formData.childName), () => this.createToothDiagram(age.months, nutritionFactor), () => this.createDetailTable(age.months, nutritionFactor), () => this.createRecommendations(data) ]); } async animateResultSections(sections) { for (let i = 0; i < sections.length; i++) { await new Promise(resolve => { setTimeout(() => { sections[i](); resolve(); }, i * 200); // 200ms delay between sections }); } } displayBMIResult(bmiResult) { const bmiContainer = document.getElementById('bmiResult'); if (!bmiResult) { bmiContainer.innerHTML = `

Thông Tin BMI

Không có đủ thông tin cân nặng và chiều cao để tính BMI.

BMI giúp đánh giá tình trạng dinh dưỡng và ảnh hưởng đến phát triển răng.

`; return; } // Tạo progress ring cho BMI const progress = this.calculateBMIProgress(bmiResult); bmiContainer.innerHTML = `
${bmiResult.bmi}
kg/m²

Tình Trạng Dinh Dưỡng

${bmiResult.category}

Phân vị: ${bmiResult.percentile}

Z-Score: ${bmiResult.zScore}

P3
${bmiResult.percentiles.p3}
P50
${bmiResult.percentiles.p50}
P97
${bmiResult.percentiles.p97}
Theo tiêu chuẩn WHO Growth Standards
`; } calculateBMIProgress(bmiResult) { // Tính % progress dựa trên vị trí trong percentile if (bmiResult.percentile === '<3') return 10; if (bmiResult.percentile === '3-15') return 30; if (bmiResult.percentile === '15-85') return 70; if (bmiResult.percentile === '85-97') return 90; return 95; } createTimelineChart(ageMonths, nutritionFactor, childName) { const ctx = document.getElementById('toothChart').getContext('2d'); // Destroy existing chart if (this.chartInstance) { this.chartInstance.destroy(); } const chartData = []; const labels = []; const backgroundColors = []; const borderColors = []; const statusData = []; Object.entries(toothEruptionData.primary).forEach(([key, tooth]) => { const adjustedTooth = { ...tooth, min: tooth.min + nutritionFactor, max: tooth.max + nutritionFactor, average: tooth.average + nutritionFactor }; chartData.push(adjustedTooth.average); labels.push(tooth.position); const status = this.getEruptionStatus(adjustedTooth, ageMonths); statusData.push(status); const colorMap = { 'not-erupted': '#e5e7eb', 'pre-erupting': '#93c5fd', 'erupting': '#fbbf24', 'erupted': '#34d399', 'late': '#fb923c', 'overdue': '#f87171' }; backgroundColors.push(colorMap[status.status]); borderColors.push(colorMap[status.status]); }); this.chartInstance = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Thời gian mọc răng dự kiến (tháng)', data: chartData, backgroundColor: backgroundColors, borderColor: borderColors, borderWidth: 2, borderRadius: 8, borderSkipped: false }, { type: 'line', label: `Tuổi hiện tại của ${childName}`, data: new Array(labels.length).fill(ageMonths), borderColor: '#dc2626', backgroundColor: 'rgba(220, 38, 38, 0.1)', borderWidth: 4, pointRadius: 6, pointBackgroundColor: '#dc2626', pointBorderColor: '#ffffff', pointBorderWidth: 3, borderDash: [10, 5], fill: false }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: `Biểu Đồ Lịch Mọc Răng Dự Kiến - ${childName}`, font: { size: 18, weight: 'bold' }, color: '#1f2937', padding: 20 }, legend: { display: true, position: 'top', labels: { font: { size: 14 }, padding: 20, usePointStyle: true } }, tooltip: { backgroundColor: 'rgba(0, 0, 0, 0.9)', titleColor: '#ffffff', bodyColor: '#ffffff', borderColor: '#4f46e5', borderWidth: 2, cornerRadius: 10, callbacks: { afterBody: function(context) { if (context[0].datasetIndex === 0) { const index = context[0].dataIndex; const status = statusData[index]; return [`Trạng thái: ${status.text}`, `Khuyến cáo: ${status.urgency === 'high' ? 'Cần tham khảo nha sĩ' : 'Theo dõi bình thường'}`]; } return null; } } } }, scales: { y: { beginAtZero: true, title: { display: true, text: 'Tuổi (tháng)', font: { size: 14, weight: 'bold' } }, grid: { color: '#f3f4f6' }, ticks: { font: { size: 12 } } }, x: { title: { display: true, text: 'Vị Trí Răng', font: { size: 14, weight: 'bold' } }, ticks: { maxRotation: 45, font: { size: 11 } }, grid: { display: false } } }, animation: { duration: 2000, easing: 'easeInOutQuart' } } }); } createToothDiagram(ageMonths, nutritionFactor) { const diagramContainer = document.getElementById('toothDiagram'); diagramContainer.innerHTML = ''; // Thêm chú thích const legend = document.createElement('div'); legend.className = 'tooth-legend'; legend.innerHTML = `
Chú thích trạng thái răng
Chưa đến thời gian
Sắp mọc
Đang mọc
Đã mọc
Mọc muộn
Trễ hẹn
`; diagramContainer.appendChild(legend); // Tạo sơ đồ răng const teethGrid = document.createElement('div'); teethGrid.className = 'teeth-grid'; Object.entries(toothEruptionData.primary).forEach(([key, tooth], index) => { const adjustedTooth = { ...tooth, min: Math.round(tooth.min + nutritionFactor), max: Math.round(tooth.max + nutritionFactor), average: Math.round(tooth.average + nutritionFactor) }; const status = this.getEruptionStatus(adjustedTooth, ageMonths); const toothElement = document.createElement('div'); toothElement.className = `tooth-item ${status.class}`; toothElement.innerHTML = `
${tooth.position.split(' ').slice(-2).join(' ')}
${status.text}
${adjustedTooth.min}-${adjustedTooth.max}T
`; // Thêm tooltip chi tiết const tooltip = ` ${tooth.position} Mã răng: ${tooth.code} Khoảng thời gian: ${adjustedTooth.min}-${adjustedTooth.max} tháng Trung bình: ${adjustedTooth.average} tháng Tuổi hiện tại: ${ageMonths} tháng Trạng thái: ${status.text} ${status.urgency === 'high' ? '\n⚠️ Nên tham khảo nha sĩ' : ''} `; toothElement.setAttribute('title', tooltip); toothElement.setAttribute('data-tooth', key); // Animation delay toothElement.style.animationDelay = `${index * 100}ms`; toothElement.addEventListener('click', () => { this.showToothDetails(tooth, adjustedTooth, status, ageMonths); }); teethGrid.appendChild(toothElement); }); diagramContainer.appendChild(teethGrid); } calculateToothProgress(tooth, ageMonths) { if (ageMonths < tooth.min) return 0; if (ageMonths > tooth.max) return 100; return Math.round(((ageMonths - tooth.min) / (tooth.max - tooth.min)) * 100); } showToothDetails(originalTooth, adjustedTooth, status, ageMonths) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; const urgencyIcon = { 'none': 'fas fa-check-circle', 'low': 'fas fa-clock', 'normal': 'fas fa-info-circle', 'medium': 'fas fa-exclamation-circle', 'high': 'fas fa-exclamation-triangle' }; const urgencyColor = { 'none': '#10b981', 'low': '#06b6d4', 'normal': '#3b82f6', 'medium': '#f59e0b', 'high': '#ef4444' }; modal.innerHTML = ` `; modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('modal-close')) { document.body.removeChild(modal); } }); document.body.appendChild(modal); // Animation setTimeout(() => { modal.querySelector('.modal-content').style.transform = 'scale(1)'; modal.style.opacity = '1'; }, 10); } getToothRecommendation(status, ageMonths, tooth) { let recommendation = ''; let recommendationClass = ''; switch (status.urgency) { case 'high': recommendation = 'Nên đưa trẻ đến nha sĩ để kiểm tra. Có thể cần chụp X-quang để đánh giá.'; recommendationClass = 'recommendation-urgent'; break; case 'medium': recommendation = 'Theo dõi thêm 1-2 tháng. Nếu vẫn chưa mọc thì nên tham khảo nha sĩ.'; recommendationClass = 'recommendation-watch'; break; case 'normal': recommendation = 'Răng đang phát triển bình thường. Tiếp tục theo dõi.'; recommendationClass = 'recommendation-normal'; break; case 'low': recommendation = 'Răng sắp mọc. Có thể massage nhẹ nướu để giúp răng mọc dễ dàng hơn.'; recommendationClass = 'recommendation-normal'; break; default: recommendation = 'Chưa đến thời gian mọc răng. Theo dõi bình thường.'; recommendationClass = 'recommendation-normal'; } return `
Khuyến Cáo

${recommendation}

`; } createDetailTable(ageMonths, nutritionFactor) { const table = document.getElementById('detailTable'); let tableHTML = ` Vị Trí Răng Mã Răng Khoảng Thời Gian Trung Bình Tiến Độ Trạng Thái Khuyến Cáo `; Object.entries(toothEruptionData.primary).forEach(([key, tooth]) => { const adjustedTooth = { ...tooth, average: Math.round(tooth.average + nutritionFactor), min: Math.round(tooth.min + nutritionFactor), max: Math.round(tooth.max + nutritionFactor) }; const status = this.getEruptionStatus(adjustedTooth, ageMonths); const progress = this.calculateToothProgress(adjustedTooth, ageMonths); let recommendation = ''; let recommendationIcon = ''; switch (status.urgency) { case 'high': recommendation = 'Tham khảo nha sĩ'; recommendationIcon = ''; break; case 'medium': recommendation = 'Theo dõi thêm'; recommendationIcon = ''; break; case 'normal': recommendation = 'Bình thường'; recommendationIcon = ''; break; case 'low': recommendation = 'Sắp mọc'; recommendationIcon = ''; break; default: recommendation = 'Theo dõi'; recommendationIcon = ''; } tableHTML += ` ${tooth.position} ${tooth.code} ${adjustedTooth.min} - ${adjustedTooth.max} tháng ${adjustedTooth.average} tháng
${progress}%
${status.text} ${recommendationIcon} ${recommendation} `; }); tableHTML += ''; table.innerHTML = tableHTML; // Thêm click event cho table rows table.addEventListener('click', (e) => { const row = e.target.closest('tr[data-tooth]'); if (row) { const toothKey = row.getAttribute('data-tooth'); const tooth = toothEruptionData.primary[toothKey]; const adjustedTooth = { ...tooth, min: Math.round(tooth.min + nutritionFactor), max: Math.round(tooth.max + nutritionFactor), average: Math.round(tooth.average + nutritionFactor) }; const status = this.getEruptionStatus(adjustedTooth, ageMonths); this.showToothDetails(tooth, adjustedTooth, status, ageMonths); } }); } createRecommendations(data) { const { formData, age, bmiResult, nutritionFactor } = data; const container = document.getElementById('recommendations'); const recommendations = []; // Khuyến cáo dinh dưỡng dựa trên BMI if (bmiResult) { if (bmiResult.status === 'danger' || bmiResult.status === 'warning') { if (bmiResult.category.includes('Suy dinh dưỡng')) { recommendations.push({ type: 'nutrition', priority: 'high', title: 'Cải Thiện Dinh Dưỡng Khẩn Cấp', content: `Trẻ có tình trạng ${bmiResult.category.toLowerCase()} (BMI: ${bmiResult.bmi}, phân vị ${bmiResult.percentile}). Tình trạng này có thể làm chậm quá trình mọc răng khoảng ${Math.abs(nutritionFactor)} tháng. Cần tăng cường dinh dưỡng với thực phẩm giàu canxi (sữa, phô mai, cá nhỏ), photpho (thịt, cá), vitamin D (ánh nắng mặt trời, dầu cá) và protein chất lượng cao.`, actions: [ 'Tham khảo bác sĩ dinh dưỡng trong 1-2 tuần', 'Xét nghiệm máu để kiểm tra thiếu hụt vitamin/khoáng chất', 'Theo dõi cân nặng hàng tuần', 'Bổ sung vitamin D3 theo chỉ định bác sĩ' ], source: 'Nghiên cứu Verma et al. (2017) - NCBI PMC5661043' }); } else { recommendations.push({ type: 'weight', priority: 'medium', title: 'Kiểm Soát Cân Nặng', content: `Trẻ có xu hướng ${bmiResult.category.toLowerCase()} (BMI: ${bmiResult.bmi}, phân vị ${bmiResult.percentile}). Thừa cân/béo phì có thể ảnh hưởng đến quá trình mọc răng và sức khỏe tổng thể.`, actions: [ 'Điều chỉnh chế độ ăn giảm đường và chất béo', 'Tăng hoạt động vận động phù hợp với lứa tuổi', 'Tham khảo bác sĩ nhi khoa về chế độ ăn', 'Theo dõi BMI định kỳ mỗi tháng' ], source: 'WHO Growth Standards & Pediatric Obesity Guidelines' }); } } else { recommendations.push({ type: 'maintain', priority: 'low', title: 'Duy Trì Dinh Dưỡng Tốt', content: `Trẻ có tình trạng dinh dưỡng bình thường (BMI: ${bmiResult.bmi}, phân vị ${bmiResult.percentile}). Hãy tiếp tục duy trì chế độ ăn cân bằng để hỗ trợ quá trình mọc răng tối ưu.`, actions: [ 'Duy trì chế độ ăn đa dạng với 4 nhóm thực phẩm', 'Đảm bảo đủ canxi và vitamin D hàng ngày', 'Theo dõi cân nặng, chiều cao định kỳ', 'Khuyến khích trẻ uống đủ nước' ], source: 'American Academy of Pediatrics Nutrition Guidelines' }); } } // Khuyến cáo về hình thức nuôi dưỡng const feedingRec = nutritionRecommendations[formData.feedingType]; if (feedingRec && formData.feedingType !== 'breastfeeding') { recommendations.push({ type: 'feeding', priority: formData.feedingType === 'formula' ? 'medium' : 'low', title: 'Tối Ưu Hóa Dinh Dưỡng Theo Hình Thức Nuôi Dưỡng', content: `${feedingRec.description}. Điều này có thể ảnh hưởng ${feedingRec.factor > 0 ? 'chậm' : 'tích cực'} đến thời gian mọc răng.`, actions: formData.feedingType === 'formula' ? [ 'Chọn sữa công thức có bổ sung canxi, photpho, vitamin D', 'Đảm bảo pha sữa đúng tỷ lệ theo hướng dẫn', 'Bổ sung thực phẩm giàu dinh dưỡng khi trẻ đủ tuổi ăn dặm', 'Tham khảo bác sĩ nhi về loại sữa phù hợp' ] : [ 'Tiếp tục kết hợp bú mẹ và sữa công thức hợp lý', 'Ưu tiên bú mẹ vào buổi sáng và tối', 'Đảm bảo mẹ có chế độ ăn đủ chất', 'Theo dõi sự phát triển của trẻ' ], source: feedingRec.source }); } // Khuyến cáo chăm sóc răng miệng recommendations.push({ type: 'dental', priority: 'high', title: 'Chăm Sóc Răng Miệng Từ Sớm', content: 'Bắt đầu vệ sinh răng miệng từ khi răng đầu tiên mọc là rất quan trọng để ngăn ngừa sâu răng sớm và tạo thói quen tốt.', actions: age.months < 6 ? [ 'Lau nhẹ nướu bằng khăn mềm ẩm 2 lần/ngày', 'Massage nướu nhẹ nhàng để kích thích tuần hoàn máu', 'Tránh cho trẻ ngậm bình sữa khi ngủ', 'Chuẩn bị đồ chơi nhai an toàn cho giai đoạn mọc răng' ] : [ 'Sử dụng bàn chải răng mềm dành cho trẻ em', 'Kem đánh răng có fluor (lượng bằng hạt gạo)', 'Đánh răng 2 lần/ngày, đặc biệt trước khi ngủ', 'Tránh đồ ngọt và nước ngọt trước khi ngủ', 'Sử dụng cup thay vì bình sữa sau 12 tháng tuổi' ], source: 'American Academy of Pediatric Dentistry Clinical Guidelines' }); // Khuyến cáo dựa trên tình trạng mọc răng const overdueTeeth = Object.entries(toothEruptionData.primary).filter(([key, tooth]) => { const adjustedMax = tooth.max + nutritionFactor; return age.months > adjustedMax + 6; }).map(([key, tooth]) => tooth.position); const lateTeeth = Object.entries(toothEruptionData.primary).filter(([key, tooth]) => { const adjustedMax = tooth.max + nutritionFactor; return age.months > adjustedMax + 3 && age.months <= adjustedMax + 6; }).map(([key, tooth]) => tooth.position); if (overdueTeeth.length > 0) { recommendations.push({ type: 'medical', priority: 'high', title: 'Cần Thăm Khám Nha Khoa', content: `Có ${overdueTeeth.length} răng chậm mọc đáng kể so với dự kiến: ${overdueTeeth.join(', ')}. Tình trạng này cần được đánh giá chuyên môn.`, actions: [ 'Đặt lịch khám nha sĩ nhi trong 1-2 tuần tới', 'Chuẩn bị danh sách các thắc mắc về sự phát triển răng', 'Có thể cần chụp X-quang toàn cảnh để đánh giá', 'Kiểm tra các yếu tố di truyền trong gia đình' ], source: 'Clinical Dental Practice Guidelines - Eruption Delay Management' }); } else if (lateTeeth.length > 0) { recommendations.push({ type: 'monitor', priority: 'medium', title: 'Theo Dõi Sát Sao', content: `Có ${lateTeeth.length} răng mọc hơi muộn: ${lateTeeth.join(', ')}. Cần theo dõi thêm 1-2 tháng.`, actions: [ 'Theo dõi hàng tuần và ghi chép tiến triển', 'Massage nhẹ nướu vùng răng chậm mọc', 'Đảm bảo đủ dinh dưỡng và vitamin D', 'Đặt lịch tái khám sau 1 tháng nếu không có tiến triển' ], source: 'Pediatric Dental Monitoring Protocols' }); } // Khuyến cáo theo dõi định kỳ recommendations.push({ type: 'routine', priority: 'low', title: 'Theo Dõi Định Kỳ', content: 'Khám nha khoa định kỳ giúp phát hiện sớm các vấn đề về răng miệng và can thiệp kịp thời.', actions: [ 'Khám nha khoa đầu tiên trước 1 tuổi hoặc trong 6 tháng sau khi răng đầu tiên mọc', 'Tái khám định kỳ mỗi 6 tháng', 'Chụp ảnh răng định kỳ để theo dõi sự phát triển', 'Tham gia các chương trình tư vấn sức khỏe răng miệng' ], source: 'American Academy of Pediatrics & AAPD Joint Recommendations' }); // Sắp xếp theo độ ưu tiên recommendations.sort((a, b) => { const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); // Hiển thị khuyến cáo container.innerHTML = recommendations.map((rec, index) => `
${rec.title}
${rec.priority === 'high' ? 'Khẩn cấp' : rec.priority === 'medium' ? 'Quan trọng' : 'Thông thường'}

${rec.content}

Hành động cần thực hiện:
    ${rec.actions.map(action => `
  • ${action}
  • `).join('')}
Nguồn khoa học: ${rec.source}
`).join(''); // Thêm animation cho recommendations const recommendationItems = container.querySelectorAll('.recommendation-item'); recommendationItems.forEach((item, index) => { item.style.animationDelay = `${index * 150}ms`; item.classList.add('fade-in-up'); }); } getRecommendationIcon(type, priority) { const icons = { 'nutrition': priority === 'high' ? 'fas fa-exclamation-triangle' : 'fas fa-apple-alt', 'weight': 'fas fa-weight', 'maintain': 'fas fa-check-circle', 'feeding': 'fas fa-baby', 'dental': 'fas fa-tooth', 'medical': 'fas fa-user-md', 'monitor': 'fas fa-search', 'routine': 'fas fa-calendar-check' }; return icons[type] || 'fas fa-info-circle'; } // =================== EXPORT & UTILITIES =================== exportToPDF() { if (!this.currentData) { this.showNotification('Chưa có dữ liệu để xuất. Vui lòng thực hiện phân tích trước.', 'warning'); return; } this.showLoading(); try { const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'mm', 'a4'); const { formData, age, bmiResult, nutritionFactor } = this.currentData; // Cấu hình font và màu sắc const primaryColor = [79, 70, 229]; const textColor = [31, 41, 55]; const accentColor = [16, 185, 129]; let currentY = 20; // Header với logo doc.setFillColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.rect(0, 0, 210, 40, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('BÁO CÁO LỊCH MỌC RĂNG SỮA', 20, 20); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); doc.text('Phân tích khoa học và khuyến cáo chuyên môn', 20, 30); currentY = 50; // Thông tin trẻ em doc.setTextColor(textColor[0], textColor[1], textColor[2]); doc.setFontSize(16); doc.setFont('helvetica', 'bold'); doc.text('THÔNG TIN TRẺ EM', 20, currentY); currentY += 10; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); const childInfo = [ `Tên trẻ: ${formData.childName}`, `Ngày sinh: ${new Date(formData.birthDate).toLocaleDateString('vi-VN')}`, `Tuổi hiện tại: ${age.years} năm ${age.remainingMonths} tháng (${age.months} tháng)`, `Giới tính: ${formData.gender === 'male' ? 'Nam' : 'Nữ'}`, formData.weight ? `Cân nặng: ${formData.weight} kg` : null, formData.height ? `Chiều cao: ${formData.height} cm` : null, `Hình thức nuôi dưỡng: ${this.getFeedingTypeText(formData.feedingType)}` ].filter(Boolean); childInfo.forEach(info => { doc.text(info, 20, currentY); currentY += 6; }); currentY += 10; // Phân tích BMI if (bmiResult) { doc.setFontSize(16); doc.setFont('helvetica', 'bold'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.text('PHÂN TÍCH TÌNH TRẠNG DINH DƯỠNG', 20, currentY); currentY += 10; doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(textColor[0], textColor[1], textColor[2]); const bmiInfo = [ `BMI: ${bmiResult.bmi} kg/m²`, `Tình trạng: ${bmiResult.category}`, `Phân vị: ${bmiResult.percentile}`, `Z-Score: ${bmiResult.zScore}` ]; bmiInfo.forEach(info => { doc.text(info, 20, currentY); currentY += 6; }); currentY += 10; } // Bảng thời gian mọc răng doc.setFontSize(16); doc.setFont('helvetica', 'bold'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.text('BẢNG THỜI GIAN MỌC RĂNG DỰ KIẾN', 20, currentY); currentY += 15; // Table headers doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Vị trí răng', 20, currentY); doc.text('Khoảng TG', 80, currentY); doc.text('TB', 110, currentY); doc.text('Trạng thái', 130, currentY); doc.text('Khuyến cáo', 160, currentY); currentY += 5; // Line under headers doc.setLineWidth(0.5); doc.line(20, currentY, 190, currentY); currentY += 5; doc.setFont('helvetica', 'normal'); Object.entries(toothEruptionData.primary).forEach(([key, tooth]) => { if (currentY > 280) { doc.addPage(); currentY = 20; } const adjustedTooth = { ...tooth, min: Math.round(tooth.min + nutritionFactor), max: Math.round(tooth.max + nutritionFactor), average: Math.round(tooth.average + nutritionFactor) }; const status = this.getEruptionStatus(adjustedTooth, age.months); // Truncate tooth name for PDF const shortName = tooth.position.length > 25 ? tooth.position.substring(0, 25) + '...' : tooth.position; doc.text(shortName, 20, currentY); doc.text(`${adjustedTooth.min}-${adjustedTooth.max}`, 80, currentY); doc.text(`${adjustedTooth.average}`, 110, currentY); doc.text(status.text, 130, currentY); const advice = status.urgency === 'high' ? 'Khám nha sĩ' : status.urgency === 'medium' ? 'Theo dõi' : 'Bình thường'; doc.text(advice, 160, currentY); currentY += 8; }); // New page for recommendations doc.addPage(); currentY = 20; doc.setFontSize(16); doc.setFont('helvetica', 'bold'); doc.setTextColor(primaryColor[0], primaryColor[1], primaryColor[2]); doc.text('KHUYẾN CÁO Y KHOA', 20, currentY); currentY += 15; // Simplified recommendations for PDF // Tiếp tục phần exportToPDF() const keyRecommendations = [ 'Vệ sinh răng miệng từ khi răng đầu tiên mọc', 'Sử dụng kem đánh răng có fluor (lượng bằng hạt gạo)', 'Khám nha khoa định kỳ mỗi 6 tháng', 'Đảm bảo đủ canxi, photpho và vitamin D', bmiResult && bmiResult.status !== 'normal' ? `Điều chỉnh dinh dưỡng do tình trạng ${bmiResult.category.toLowerCase()}` : null ].filter(Boolean); doc.setFontSize(11); doc.setFont('helvetica', 'normal'); doc.setTextColor(textColor[0], textColor[1], textColor[2]); keyRecommendations.forEach((rec, index) => { if (currentY > 280) { doc.addPage(); currentY = 20; } doc.text(`${index + 1}. ${rec}`, 20, currentY); currentY += 8; }); // Footer currentY += 20; doc.setFontSize(9); doc.setTextColor(100, 100, 100); doc.text(`Báo cáo được tạo ngày ${new Date().toLocaleDateString('vi-VN')} lúc ${new Date().toLocaleTimeString('vi-VN')}`, 20, currentY); currentY += 5; doc.text('Công cụ được phát triển bởi DrNinhMai.com', 20, currentY); currentY += 5; doc.text('Dựa trên nghiên cứu từ NCBI PMC, Cleveland Clinic & các tạp chí y khoa uy tín', 20, currentY); currentY += 5; doc.text('Nguồn: Ogodescu et al. (2022), WHO Growth Standards, AAPD Guidelines', 20, currentY); // Lưu file const fileName = `BaoCao_LichMocRang_${formData.childName}_${new Date().toLocaleDateString('vi-VN').replace(/\//g, '-')}.pdf`; doc.save(fileName); this.showNotification('Đã tải xuống báo cáo PDF thành công!', 'success'); } catch (error) { console.error('Lỗi xuất PDF:', error); this.showNotification('Có lỗi xảy ra khi xuất PDF. Vui lòng thử lại.', 'error'); } finally { this.hideLoading(); } } getFeedingTypeText(feedingType) { const feedingTexts = { 'breastfeeding': 'Bú mẹ hoàn toàn', 'mixed': 'Bú mẹ + Sữa công thức', 'formula': 'Sữa công thức' }; return feedingTexts[feedingType] || feedingType; } showEmailDialog() { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.innerHTML = ` `; const closeModal = () => { modal.style.opacity = '0'; modal.querySelector('.modal-content').style.transform = 'scale(0.9)'; setTimeout(() => { if (document.body.contains(modal)) { document.body.removeChild(modal); } }, 300); }; modal.querySelector('.modal-close').addEventListener('click', closeModal); modal.querySelector('.modal-cancel').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); modal.querySelector('#emailForm').addEventListener('submit', (e) => { e.preventDefault(); this.sendEmailReport({ recipients: document.getElementById('recipientEmail').value, subject: document.getElementById('emailSubject').value, message: document.getElementById('emailMessage').value, includeChart: document.getElementById('includeChart').checked, includeRecommendations: document.getElementById('includeRecommendations').checked }); closeModal(); }); document.body.appendChild(modal); // Animation setTimeout(() => { modal.style.opacity = '1'; modal.querySelector('.modal-content').style.transform = 'scale(1)'; }, 10); // Focus vào email input setTimeout(() => { document.getElementById('recipientEmail').focus(); }, 400); } async sendEmailReport(emailData) { this.showLoading(); try { // Tích hợp với EmailJS hoặc service gửi email // Đây là template để tích hợp với EmailJS const emailContent = this.generateEmailContent(emailData); // Simulate API call - thay bằng EmailJS service await new Promise(resolve => setTimeout(resolve, 2000)); this.showNotification( `Báo cáo đã được gửi đến ${emailData.recipients}. Vui lòng kiểm tra hộp thư.`, 'success' ); // Tracking this.trackEvent('email_sent', { recipients_count: emailData.recipients.split(',').length, include_chart: emailData.includeChart, include_recommendations: emailData.includeRecommendations }); } catch (error) { console.error('Lỗi gửi email:', error); this.showNotification( 'Không thể gửi email lúc này. Vui lòng sử dụng tính năng tải PDF và gửi thủ công.', 'error' ); } finally { this.hideLoading(); } } generateEmailContent(emailData) { if (!this.currentData) return ''; const { formData, age, bmiResult } = this.currentData; return ` Báo cáo lịch mọc răng sữa

Báo Cáo Lịch Mọc Răng Sữa

DrNinhMai.com - Công cụ phân tích khoa học

Thông Tin Trẻ Em

Tên: ${formData.childName}

Tuổi: ${age.years} năm ${age.remainingMonths} tháng

Giới tính: ${formData.gender === 'male' ? 'Nam' : 'Nữ'}

${bmiResult ? `

BMI: ${bmiResult.bmi} kg/m² (${bmiResult.category})

` : ''}

Tóm Tắt Phân Tích

${emailData.message}

${emailData.includeRecommendations ? '

Khuyến cáo y khoa chi tiết được đính kèm trong báo cáo PDF.

' : ''}
`.trim(); } printReport() { if (!this.currentData) { this.showNotification('Chưa có dữ liệu để in. Vui lòng thực hiện phân tích trước.', 'warning'); return; } // Ẩn các element không cần thiết khi in const elementsToHide = [ '.export-section', '.loading-overlay', 'button', '.modal-overlay' ]; elementsToHide.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { el.style.display = 'none'; }); }); // Thêm style cho in ấn const printStyles = document.createElement('style'); printStyles.innerHTML = ` @media print { body { background: white !important; font-size: 12pt; } .container { max-width: none !important; margin: 0 !important; padding: 0 !important; } .card { break-inside: avoid; box-shadow: none !important; border: 1px solid #ccc !important; margin-bottom: 20pt !important; page-break-inside: avoid; } .header-section { background: none !important; color: black !important; } .logo-area { background: none !important; border: 2px solid #4f46e5 !important; } h1, h2, h3 { color: black !important; } .timeline-container { height: 300px !important; } .export-section { display: none !important; } .button-group { display: none !important; } .modal-overlay { display: none !important; } .loading-overlay { display: none !important; } } `; document.head.appendChild(printStyles); // In window.print(); // Restore các element đã ẩn setTimeout(() => { elementsToHide.forEach(selector => { const elements = document.querySelectorAll(selector); elements.forEach(el => { el.style.display = ''; }); }); // Remove print styles document.head.removeChild(printStyles); }, 1000); this.trackEvent('report_printed', { child_name: this.currentData.formData.childName, child_age_months: this.currentData.age.months }); } resetForm() { // Reset form document.getElementById('toothForm').reset(); // Ẩn kết quả const resultsSection = document.getElementById('resultsSection'); resultsSection.style.transition = 'all 0.3s ease-out'; resultsSection.style.opacity = '0'; resultsSection.style.transform = 'translateY(-20px)'; setTimeout(() => { resultsSection.style.display = 'none'; resultsSection.style.opacity = ''; resultsSection.style.transform = ''; }, 300); // Reset validation const inputs = document.querySelectorAll('input, select'); inputs.forEach(input => { input.classList.remove('error', 'success'); this.removeErrorMessage(input); }); // Destroy chart if (this.chartInstance) { this.chartInstance.destroy(); this.chartInstance = null; } // Clear current data this.currentData = null; // Focus vào tên trẻ document.getElementById('childName').focus(); this.showNotification('Đã làm mới form thành công!', 'info'); } // =================== NOTIFICATION & UTILITIES =================== showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification notification-${type}`; const icons = { 'success': 'fas fa-check-circle', 'error': 'fas fa-exclamation-circle', 'warning': 'fas fa-exclamation-triangle', 'info': 'fas fa-info-circle' }; notification.innerHTML = ` ${message} `; // Position notifications const existingNotifications = document.querySelectorAll('.notification'); const topPosition = 20 + (existingNotifications.length * 70); notification.style.top = `${topPosition}px`; document.body.appendChild(notification); // Auto remove const removeNotification = () => { notification.style.animation = 'slideOutRight 0.3s ease-in'; setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 300); }; // Close button notification.querySelector('.notification-close').addEventListener('click', removeNotification); // Auto remove after 5 seconds setTimeout(removeNotification, 5000); // Slide in animation setTimeout(() => { notification.style.animation = 'slideInRight 0.3s ease-out'; }, 10); } trackEvent(eventName, eventData = {}) { // Google Analytics tracking if (typeof gtag !== 'undefined') { gtag('event', eventName, { ...eventData, event_category: 'tooth_eruption_calculator', event_label: eventData.child_name || 'anonymous' }); } // Console log for debugging console.log('Event tracked:', eventName, eventData); } addModalStyles() { const modalStyles = ` .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; } .modal-content { background: white; border-radius: 16px; max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto; transform: scale(0.9); transition: transform 0.3s ease; box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25); } .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 25px; border-bottom: 1px solid #e5e7eb; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 16px 16px 0 0; } .modal-header h3 { margin: 0; display: flex; align-items: center; gap: 10px; font-size: 1.25rem; } .modal-close { background: rgba(255, 255, 255, 0.2); border: none; color: white; font-size: 24px; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; } .modal-close:hover { background: rgba(255, 255, 255, 0.3); } .modal-body { padding: 25px; } .tooth-detail-modal .modal-body { padding: 0; } .tooth-detail-grid { display: grid; grid-template-columns: auto 1fr; gap: 25px; padding: 25px; } .tooth-visual { text-align: center; } .big-tooth-icon { font-size: 4rem; padding: 20px; border-radius: 50%; margin-bottom: 15px; display: inline-block; } .status-badge { padding: 8px 16px; border-radius: 25px; color: white; font-weight: 600; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 5px; } .tooth-details h4 { margin-top: 0; margin-bottom: 20px; color: #1f2937; font-size: 1.2rem; } .detail-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f3f4f6; } .detail-row .label { font-weight: 600; color: #6b7280; } .detail-row .value { font-weight: 500; color: #1f2937; } .progress-section { margin: 20px 0; } .progress-bar-large { width: 100%; height: 12px; background: #e5e7eb; border-radius: 6px; overflow: hidden; margin: 8px 0; } .progress-fill { height: 100%; transition: width 0.8s ease; border-radius: 6px; } .progress-text { font-size: 0.9rem; font-weight: 600; color: #6b7280; } .recommendation-box { margin-top: 20px; padding: 20px; border-radius: 12px; border-left: 4px solid; } .recommendation-urgent { background: #fef2f2; border-color: #ef4444; } .recommendation-watch { background: #fffbeb; border-color: #f59e0b; } .recommendation-normal { background: #f0fdf4; border-color: #10b981; } .recommendation-box h5 { margin: 0 0 10px 0; display: flex; align-items: center; gap: 8px; font-size: 1rem; } .recommendation-urgent h5 { color: #dc2626; } .recommendation-watch h5 { color: #d97706; } .recommendation-normal h5 { color: #059669; } .recommendation-box p { margin: 0; line-height: 1.6; color: #374151; } .email-modal .form-group { margin-bottom: 20px; } .email-modal label { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-weight: 600; color: #374151; } .form-hint { font-size: 0.8rem; color: #6b7280; margin-top: 4px; display: block; } .email-options { display: flex; flex-direction: column; gap: 10px; margin: 20px 0; } .checkbox-label { display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 10px; border-radius: 8px; transition: background-color 0.2s; } .checkbox-label:hover { background: #f9fafb; } .checkmark { width: 20px; height: 20px; border: 2px solid #d1d5db; border-radius: 4px; position: relative; transition: all 0.2s; } .checkbox-label input[type="checkbox"] { display: none; } .checkbox-label input[type="checkbox"]:checked + .checkmark { background: #4f46e5; border-color: #4f46e5; } .checkbox-label input[type="checkbox"]:checked + .checkmark::after { content: '✓'; position: absolute; color: white; font-size: 14px; top: -2px; left: 3px; } .badge { padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; display: inline-block; } .badge-not-erupted { background: #f3f4f6; color: #6b7280; } .badge-pre-erupting { background: #dbeafe; color: #2563eb; } .badge-erupting { background: #fef3c7; color: #d97706; } .badge-erupted { background: #d1fae5; color: #059669; } .badge-late { background: #fed7aa; color: #ea580c; } .badge-overdue { background: #fee2e2; color: #dc2626; } .notification { position: fixed; right: 20px; background: white; border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); z-index: 1001; display: flex; align-items: center; gap: 12px; max-width: 400px; border-left: 4px solid; animation: slideInRight 0.3s ease-out; } .notification-success { border-color: #10b981; background: linear-gradient(135deg, #ecfdf5, #d1fae5); } .notification-error { border-color: #ef4444; background: linear-gradient(135deg, #fef2f2, #fee2e2); } .notification-warning { border-color: #f59e0b; background: linear-gradient(135deg, #fffbeb, #fef3c7); } .notification-info { border-color: #3b82f6; background: linear-gradient(135deg, #eff6ff, #dbeafe); } .notification-success i { color: #059669; } .notification-error i { color: #dc2626; } .notification-warning i { color: #d97706; } .notification-info i { color: #2563eb; } .notification-close { background: none; border: none; font-size: 18px; cursor: pointer; opacity: 0.6; margin-left: auto; } .notification-close:hover { opacity: 1; } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } .text-danger { color: #dc2626; } .text-warning { color: #d97706; } .text-success { color: #059669; } .text-info { color: #2563eb; } .text-muted { color: #6b7280; } /* Responsive modal */ @media (max-width: 768px) { .modal-content { width: 95%; margin: 10px; } .tooth-detail-grid { grid-template-columns: 1fr; text-align: center; } .modal-body { padding: 20px; } .tooth-detail-grid { padding: 20px; } } `; const styleSheet = document.createElement('style'); styleSheet.textContent = modalStyles; document.head.appendChild(styleSheet); } } // =================== ADDITIONAL STYLES =================== const additionalStyles = ` /* Enhanced form validation styles */ input.error, select.error { border-color: #ef4444 !important; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important; background-color: #fef2f2; } input.success, select.success { border-color: #10b981 !important; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1) !important; background-color: #f0fdf4; } .error-message { color: #dc2626; font-size: 0.8rem; margin-top: 4px; display: flex; align-items: center; gap: 5px; } .error-message::before { content: '⚠️'; font-size: 0.9rem; } /* Enhanced BMI display */ .bmi-info-only { display: flex; align-items: center; gap: 20px; padding: 20px; background: linear-gradient(135deg, #f3f4f6, #e5e7eb); border-radius: 12px; } .info-icon { font-size: 2rem; color: #6b7280; } .bmi-progress-ring { position: relative; display: inline-block; } .bmi-value { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; } .bmi-number { font-size: 1.8rem; font-weight: 700; color: #1f2937; } .bmi-unit { font-size: 0.9rem; color: #6b7280; } .percentile-bars { margin: 15px 0; } .percentile-bar { display: flex; align-items: center; gap: 10px; margin: 8px 0; font-size: 0.8rem; } .percentile-bar span { min-width: 25px; font-weight: 600; color: #6b7280; } .bar { flex: 1; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; } .fill { height: 100%; background: linear-gradient(90deg, #4f46e5, #7c3aed); transition: width 1s ease; } /* Enhanced tooth diagram */ .tooth-legend { margin-bottom: 20px; padding: 20px; background: #f9fafb; border-radius: 12px; border: 1px solid #e5e7eb; } .legend-title { display: flex; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 15px; color: #374151; } .legend-items { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; } .legend-item { display: flex; align-items: center; gap: 10px; font-size: 0.9rem; } .legend-color { width: 20px; height: 20px; border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.1); } .teeth-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; margin-top: 20px; } .tooth-item { background: white; border: 2px solid #e5e7eb; border-radius: 12px; padding: 20px; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; } .tooth-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: currentColor; opacity: 0.7; } .tooth-item:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15); border-color: currentColor; } .tooth-icon { font-size: 2rem; margin-bottom: 10px; opacity: 0.8; } .tooth-info { text-align: center; } .tooth-name { font-weight: 600; font-size: 0.9rem; color: #1f2937; margin-bottom: 5px; } .tooth-status { font-size: 0.8rem; font-weight: 500; margin-bottom: 8px; } .tooth-time { font-size: 0.7rem; color: #6b7280; font-weight: 600; } .tooth-progress { margin-top: 12px; } .progress-bar { width: 100%; height: 4px; background: rgba(0, 0, 0, 0.1); border-radius: 2px; overflow: hidden; } .progress-fill { height: 100%; background: currentColor; transition: width 1s ease; border-radius: 2px; } /* Tooth status colors */ .tooth-not-erupted { color: #9ca3af; background: #f9fafb; } .tooth-pre-erupting { color: #2563eb; background: #eff6ff; } .tooth-erupting { color: #d97706; background: #fffbeb; } .tooth-erupted { color: #059669; background: #f0fdf4; } .tooth-late { color: #ea580c; background: #fff7ed; } .tooth-overdue { color: #dc2626; background: #fef2f2; } /* Enhanced table */ .table-container { overflow-x: auto; border-radius: 12px; border: 1px solid #e5e7eb; } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; } th { background: linear-gradient(135deg, #f9fafb, #f3f4f6); padding: 16px 12px; text-align: left; font-weight: 600; color: #374151; border-bottom: 2px solid #e5e7eb; position: sticky; top: 0; z-index: 10; } td { padding: 12px; border-bottom: 1px solid #f3f4f6; vertical-align: middle; } tr:hover { background: #f9fafb; cursor: pointer; } .table-progress { display: flex; align-items: center; gap: 10px; min-width: 100px; } .progress-bar-mini { flex: 1; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; } .progress-fill-mini { height: 100%; background: linear-gradient(90deg, #4f46e5, #7c3aed); border-radius: 3px; transition: width 0.8s ease; } .progress-percent { font-size: 0.8rem; font-weight: 600; color: #6b7280; min-width: 35px; } /* Enhanced recommendations */ .recommendation-item { background: white; border-radius: 16px; padding: 25px; margin-bottom: 20px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); border: 1px solid rgba(0, 0, 0, 0.05); position: relative; overflow: hidden; opacity: 0; transform: translateY(20px); animation: fadeInUp 0.6s ease forwards; } .recommendation-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, #4f46e5, #7c3aed); } .recommendation-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 15px; } .recommendation-header h5 { margin: 0; font-size: 1.1rem; font-weight: 600; color: #1f2937; display: flex; align-items: center; gap: 10px; } .priority-badge { padding: 4px 12px; border-radius: 20px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .priority-high { background: linear-gradient(135deg, #fee2e2, #fecaca); color: #dc2626; } .priority-medium { background: linear-gradient(135deg, #fef3c7, #fde68a); color: #d97706; } .priority-low { background: linear-gradient(135deg, #e0f2fe, #b3e5fc); color: #0369a1; } .recommendation-content p { line-height: 1.7; color: #4b5563; margin-bottom: 15px; } .action-list h6 { color: #374151; font-size: 0.95rem; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; } .action-list ul { list-style: none; padding: 0; margin: 0; } .action-list li { padding: 8px 0 8px 20px; position: relative; color: #4b5563; line-height: 1.6; } .action-list li::before { content: '✓'; position: absolute; left: 0; color: #10b981; font-weight: bold; } .recommendation-source { margin-top: 20px; padding-top: 15px; border-top: 1px solid #f3f4f6; } .recommendation-source small { color: #6b7280; font-style: italic; display: flex; align-items: center; gap: 5px; } .fade-in-up { animation: fadeInUp 0.6s ease forwards; } @keyframes fadeInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } /* Enhanced responsive design */ @media (max-width: 768px) { .teeth-grid { grid-template-columns: 1fr; } .legend-items { grid-template-columns: 1fr; } .recommendation-header { flex-direction: column; gap: 10px; } .priority-badge { align-self: flex-start; } .table-container { font-size: 0.8rem; } th, td { padding: 8px 6px; } } @media (max-width: 480px) { .bmi-result { grid-template-columns: 1fr; text-align: center; } .percentile-bars { display: none; } .tooth-item { padding: 15px; } } `; // Thêm styles bổ sung const additionalStyleSheet = document.createElement('style'); additionalStyleSheet.textContent = additionalStyles; document.head.appendChild(additionalStyleSheet); // =================== KHỞI TẠO ỨNG DỤNG =================== // Khởi tạo ứng dụng khi DOM ready document.addEventListener('DOMContentLoaded', () => { // Kiểm tra các thư viện cần thiết if (typeof Chart === 'undefined') { console.error('Chart.js library không được tải. Vui lòng kiểm tra CDN.'); return; } if (typeof jsPDF === 'undefined') { console.warn('jsPDF library không được tải. Tính năng xuất PDF sẽ không hoạt động.'); } // Khởi tạo calculator try { window.toothCalculator = new ToothEruptionCalculator(); console.log('Tooth Eruption Calculator đã được khởi tạo thành công!'); // Hiển thị thông báo chào mừng setTimeout(() => { window.toothCalculator.showNotification( 'Chào mừng bạn đến với công cụ phân tích lịch mọc răng sữa chuyên nghiệp!', 'info' ); }, 1000); } catch (error) { console.error('Lỗi khởi tạo ứng dụng:', error); alert('Có lỗi xảy ra khi khởi tạo ứng dụng. Vui lòng tải lại trang.'); } }); // Error handling toàn cục window.addEventListener('error', (event) => { console.error('Global error:', event.error); if (window.toothCalculator) { window.toothCalculator.showNotification( 'Đã xảy ra lỗi không mong muốn. Vui lòng tải lại trang nếu sự cố tiếp diễn.', 'error' ); } }); // Performance monitoring window.addEventListener('load', () => { const loadTime = performance.now(); console.log(`Ứng dụng đã tải hoàn tất trong ${Math.round(loadTime)}ms`); if (window.toothCalculator) { window.toothCalculator.trackEvent('app_loaded', { load_time: Math.round(loadTime), user_agent: navigator.userAgent, screen_resolution: `${screen.width}x${screen.height}` }); } }); // Service Worker registration cho PWA (tùy chọn) if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('ServiceWorker đã đăng ký thành công'); }) .catch(error => { console.log('ServiceWorker đăng ký thất bại'); }); }); } // Export cho testing if (typeof module !== 'undefined' && module.exports) { module.exports = { ToothEruptionCalculator, toothEruptionData, bmiPercentiles }; }