🔐 MiAutoPro

🚗 MiAutoPro v4.1

Suzuki Alto 1.1 F10D • Liqui-Moly 10W40

📋 Documentos Legales

📤 Cargar PDF Local

⚙️ Cambio de Aceite

⛽ Combustible por Mes

✅ Checklist Seguridad

// ====================================== // SISTEMA DE CÓDIGOS DE ACTIVACIÓN // ====================================== let deviceId = null; let adminMode = false; function generateDeviceId() { if (localStorage.getItem('deviceId')) { return localStorage.getItem('deviceId'); } const id = 'DEV-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); localStorage.setItem('deviceId', id); return id; } async function initActivationScreen() { deviceId = generateDeviceId(); document.getElementById('deviceId').textContent = deviceId; const activation = await getData('config', 'activation'); if (!activation || !activation.isActivated) { document.getElementById('activationScreen').style.display = 'flex'; document.getElementById('authScreen').style.display = 'none'; } else { document.getElementById('activationScreen').style.display = 'none'; await initAuth(); } } function toggleAdminPanel() { document.getElementById('adminPanel').style.display = document.getElementById('adminPanel').style.display === 'none' ? 'block' : 'none'; } async function loginAdmin() { const password = document.getElementById('adminPassword').value; const adminData = await getData('config', 'adminAuth'); if (!adminData) { if (!password) { showToast('Establece una contraseña de admin primero', 'error'); return; } // Crear admin por primera vez const adminHash = await simpleHash(password); await saveData('config', { id: 'adminAuth', adminHash: adminHash }); adminMode = true; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'block'; refreshCodesList(); showToast('✓ Panel de admin activado', 'success'); return; } const passwordHash = await simpleHash(password); if (passwordHash === adminData.adminHash) { adminMode = true; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'block'; refreshCodesList(); showToast('✓ Admin autenticado', 'success'); } else { showToast('❌ Contraseña de admin incorrecta', 'error'); } } function logoutAdmin() { adminMode = false; document.getElementById('adminPanel').style.display = 'block'; document.getElementById('adminDashboard').style.display = 'none'; document.getElementById('adminPassword').value = ''; showToast('✓ Sesión de admin cerrada', 'success'); } async function generateNewCode() { if (!adminMode) { showToast('Debes estar autenticado como admin', 'error'); return; } const name = document.getElementById('codeName').value; const uses = parseInt(document.getElementById('codeUses').value); if (!name || !uses) { showToast('Completa todos los campos', 'error'); return; } const code = 'ACT-' + Math.random().toString(36).substr(2, 4).toUpperCase() + '-' + Math.random().toString(36).substr(2, 4).toUpperCase() + '-' + Math.random().toString(36).substr(2, 4).toUpperCase(); const codeData = { id: `code_${Date.now()}`, code: code, name: name, maxUses: uses, currentUses: 0, createdDate: new Date().toISOString(), lastUsed: null, usedBy: [], isActive: true }; await saveData('config', codeData); document.getElementById('codeName').value = ''; document.getElementById('codeUses').value = '1'; refreshCodesList(); showToast('✓ Código generado: ' + code, 'success'); } async function refreshCodesList() { const codes = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('config_code_')) { codes.push(JSON.parse(localStorage.getItem(key))); } } const html = codes.filter(c => c.isActive).map(c => `
${c.code} ${c.name} • ${c.currentUses}/${c.maxUses} usos
`).join(''); document.getElementById('codesListContainer').innerHTML = html || 'No hay códigos activos'; } async function revokeCode(codeId) { if (confirm('¿Revocar este código?')) { const code = await getData('config', codeId); code.isActive = false; await saveData('config', code); refreshCodesList(); showToast('✓ Código revocado', 'success'); } } async function validateActivationCode() { const code = document.getElementById('activationCode').value.trim().toUpperCase(); if (!code) { showToast('Ingresa un código de activación', 'error'); return; } // Buscar el código let foundCode = null; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('config_code_')) { const c = JSON.parse(localStorage.getItem(key)); if (c.code === code && c.isActive && c.currentUses < c.maxUses) { foundCode = c; break; } } } if (!foundCode) { showToast('❌ Código inválido o expirado', 'error'); return; } // Registrar activación foundCode.currentUses++; foundCode.lastUsed = new Date().toISOString(); foundCode.usedBy.push({ deviceId: deviceId, date: new Date().toISOString() }); await saveData('config', foundCode); // Marcar como activado const activation = { id: 'activation', isActivated: true, activationCode: code, activationDate: new Date().toISOString(), deviceId: deviceId }; await saveData('config', activation); showToast('✓ Código validado. Ahora crea tu contraseña.', 'success'); document.getElementById('activationScreen').style.display = 'none'; document.getElementById('authScreen').style.display = 'flex'; showSetupView(); } async function exportCodes() { const codes = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('config_code_')) { codes.push(JSON.parse(localStorage.getItem(key))); } } const exportData = { exportDate: new Date().toISOString(), codes: codes, totalCodes: codes.length }; const json = JSON.stringify(exportData, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `miauto-codes-${new Date().getTime()}.json`; link.click(); showToast('✓ Códigos exportados', 'success'); } let isAuthenticated = false; const HASH_ITERATIONS = 10000; async function simpleHash(str) { const encoder = new TextEncoder(); const data = encoder.encode(str); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } async function initAuth() { const authData = await getData('config', 'auth'); if (!authData) { showLoginView(); return; } isAuthenticated = false; } function showLoginView() { document.getElementById('loginView').style.display = 'block'; document.getElementById('setupView').style.display = 'none'; document.getElementById('recoveryView').style.display = 'none'; document.getElementById('resetPasswordView').style.display = 'none'; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'none'; } function showSetupView() { document.getElementById('loginView').style.display = 'none'; document.getElementById('setupView').style.display = 'block'; document.getElementById('recoveryView').style.display = 'none'; document.getElementById('resetPasswordView').style.display = 'none'; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'none'; } function showRecoveryView() { document.getElementById('loginView').style.display = 'none'; document.getElementById('setupView').style.display = 'none'; document.getElementById('recoveryView').style.display = 'block'; document.getElementById('resetPasswordView').style.display = 'none'; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'none'; } function showResetPasswordView() { document.getElementById('loginView').style.display = 'none'; document.getElementById('setupView').style.display = 'none'; document.getElementById('recoveryView').style.display = 'none'; document.getElementById('resetPasswordView').style.display = 'block'; document.getElementById('adminPanel').style.display = 'none'; document.getElementById('adminDashboard').style.display = 'none'; } async function setupPassword() { const pwd = document.getElementById('setupPassword').value; const pwd2 = document.getElementById('setupPassword2').value; const q1 = document.getElementById('securityQ1').value; const q2 = document.getElementById('securityQ2').value; const q3 = document.getElementById('securityQ3').value; if (!pwd || pwd.length < 6) { showToast('La contraseña debe tener al menos 6 caracteres', 'error'); return; } if (pwd !== pwd2) { showToast('Las contraseñas no coinciden', 'error'); return; } if (!q1 || !q2 || !q3) { showToast('Debes responder todas las preguntas de seguridad', 'error'); return; } const passwordHash = await simpleHash(pwd); const q1Hash = await simpleHash(q1.toLowerCase().trim()); const q2Hash = await simpleHash(q2.toLowerCase().trim()); const q3Hash = await simpleHash(q3.toLowerCase().trim()); const authData = { id: 'auth', passwordHash: passwordHash, q1Hash: q1Hash, q2Hash: q2Hash, q3Hash: q3Hash, createdDate: new Date().toISOString() }; await saveData('config', authData); showToast('✓ Contraseña creada exitosamente', 'success'); // Limpiar campos document.getElementById('setupPassword').value = ''; document.getElementById('setupPassword2').value = ''; document.getElementById('securityQ1').value = ''; document.getElementById('securityQ2').value = ''; document.getElementById('securityQ3').value = ''; showLoginView(); } async function loginApp() { const password = document.getElementById('loginPassword').value; if (!password) { showToast('Ingresa tu contraseña', 'error'); return; } const authData = await getData('config', 'auth'); if (!authData) { showToast('No hay contraseña configurada. Crea una primero.', 'warning'); showSetupView(); return; } const passwordHash = await simpleHash(password); if (passwordHash === authData.passwordHash) { isAuthenticated = true; document.getElementById('authScreen').style.display = 'none'; document.getElementById('loginPassword').value = ''; showToast('✓ Sesión iniciada', 'success'); } else { showToast('❌ Contraseña incorrecta', 'error'); } } async function verifySecurityAnswers() { const q1 = document.getElementById('recoveryQ1').value.toLowerCase().trim(); const q2 = document.getElementById('recoveryQ2').value.toLowerCase().trim(); const q3 = document.getElementById('recoveryQ3').value.toLowerCase().trim(); const authData = await getData('config', 'auth'); if (!authData) { showToast('No hay respuestas de seguridad configuradas', 'error'); return; } const q1Hash = await simpleHash(q1); const q2Hash = await simpleHash(q2); const q3Hash = await simpleHash(q3); if (q1Hash === authData.q1Hash && q2Hash === authData.q2Hash && q3Hash === authData.q3Hash) { document.getElementById('recoveryQ1').value = ''; document.getElementById('recoveryQ2').value = ''; document.getElementById('recoveryQ3').value = ''; showResetPasswordView(); showToast('✓ Respuestas correctas', 'success'); } else { showToast('❌ Respuestas incorrectas. Intenta de nuevo.', 'error'); } } async function resetPassword() { const pwd = document.getElementById('resetPassword').value; const pwd2 = document.getElementById('resetPassword2').value; if (!pwd || pwd.length < 6) { showToast('La contraseña debe tener al menos 6 caracteres', 'error'); return; } if (pwd !== pwd2) { showToast('Las contraseñas no coinciden', 'error'); return; } const authData = await getData('config', 'auth'); authData.passwordHash = await simpleHash(pwd); await saveData('config', authData); document.getElementById('resetPassword').value = ''; document.getElementById('resetPassword2').value = ''; showToast('✓ Contraseña cambiada. Inicia sesión con la nueva.', 'success'); showLoginView(); } async function changePassword() { const currentPwd = document.getElementById('currentPassword').value; const newPwd = document.getElementById('newPassword').value; const newPwd2 = document.getElementById('confirmNewPassword').value; if (!currentPwd || !newPwd) { showToast('Completa todos los campos', 'error'); return; } if (newPwd.length < 6) { showToast('La nueva contraseña debe tener al menos 6 caracteres', 'error'); return; } if (newPwd !== newPwd2) { showToast('Las nuevas contraseñas no coinciden', 'error'); return; } const authData = await getData('config', 'auth'); const currentHash = await simpleHash(currentPwd); if (currentHash !== authData.passwordHash) { showToast('❌ Contraseña actual incorrecta', 'error'); return; } if (currentPwd === newPwd) { showToast('La nueva contraseña debe ser diferente', 'warning'); return; } authData.passwordHash = await simpleHash(newPwd); await saveData('config', authData); document.getElementById('currentPassword').value = ''; document.getElementById('newPassword').value = ''; document.getElementById('confirmNewPassword').value = ''; closeSecurityModal(); showToast('✓ Contraseña cambiad exitosamente', 'success'); } async function updateSecurityQuestions() { const q1 = document.getElementById('updateQ1').value.toLowerCase().trim(); const q2 = document.getElementById('updateQ2').value.toLowerCase().trim(); const q3 = document.getElementById('updateQ3').value.toLowerCase().trim(); if (!q1 || !q2 || !q3) { showToast('Debes responder todas las preguntas', 'error'); return; } const authData = await getData('config', 'auth'); authData.q1Hash = await simpleHash(q1); authData.q2Hash = await simpleHash(q2); authData.q3Hash = await simpleHash(q3); await saveData('config', authData); document.getElementById('updateQ1').value = ''; document.getElementById('updateQ2').value = ''; document.getElementById('updateQ3').value = ''; closeSecurityModal(); showToast('✓ Preguntas de seguridad actualizadas', 'success'); } function openSecurityModal() { document.getElementById('securityModal').classList.add('active'); } function closeSecurityModal() { document.getElementById('securityModal').classList.remove('active'); } function handleAuthKeypress(event) { if (event.key === 'Enter') { loginApp(); } } function logoutApp() { isAuthenticated = false; document.getElementById('authScreen').style.display = 'flex'; document.getElementById('loginPassword').value = ''; showToast('✓ Sesión cerrada', 'success'); } let deferredPrompt = null; let isAppInstalled = false; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallPrompt(); }); window.addEventListener('appinstalled', () => { isAppInstalled = true; hideInstallPrompt(); updateAppStatus(); showToast('✓ MiAutoPro instalado como app', 'success'); }); function showInstallPrompt() { if (!isAppInstalled && deferredPrompt) { document.getElementById('installPrompt').style.display = 'block'; } } function hideInstallPrompt() { document.getElementById('installPrompt').style.display = 'none'; } function dismissInstall() { hideInstallPrompt(); deferredPrompt = null; } async function installPWA() { if (deferredPrompt) { deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; if (outcome === 'accepted') { isAppInstalled = true; hideInstallPrompt(); updateAppStatus(); } deferredPrompt = null; } } function updateAppStatus() { const statusDiv = document.getElementById('appStatus'); const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches || isAppInstalled; if (isStandalone || isAppInstalled) { statusDiv.innerHTML = '✓ Ejecutándose como app • Funcionando completamente offline'; statusDiv.style.color = '#065f46'; statusDiv.style.background = '#d1fae5'; statusDiv.style.padding = '0.5rem'; statusDiv.style.borderRadius = '6px'; } else { statusDiv.innerHTML = '🌐 Ejecutándose en navegador'; statusDiv.style.color = '#6b7280'; } } // ====================================== // CONFIGURACIÓN Y BASE DE DATOS // ====================================== const DB_NAME = 'MiAutoPro'; const DB_VERSION = 1; let db = null; const DOCS_CONFIG = { soap: { label: 'SOAP (Seguro Obligatorio)', icon: '🛡️', color: '#ec4899', alertDays: 30 }, permiso: { label: 'Permiso de Circulación', icon: '🚗', color: '#f59e0b', alertDays: 30, hasQuotas: true }, revision: { label: 'Revisión Técnica', icon: '🔧', color: '#10b981', alertDays: 30, monthlyReminder: true }, padron: { label: 'Padrón del Auto', icon: '📋', color: '#8b5cf6', alertDays: 30 }, licencia: { label: 'Licencia de Conducir', icon: '📜', color: '#3b82f6', alertDays: 30 } }; const CHECKLIST_ITEMS = [ { id: 'botiquin', label: 'Botiquín de Primeros Auxilios', hasExpiry: true }, { id: 'extintor', label: 'Extintor', hasExpiry: true }, { id: 'chaleco1', label: 'Chaleco Fluorescente #1', hasExpiry: false }, { id: 'chaleco2', label: 'Chaleco Fluorescente #2', hasExpiry: false }, { id: 'triangulos', label: 'Triángulos Reflectantes', hasExpiry: false }, { id: 'cables', label: 'Cables de Arranque', hasExpiry: false }, { id: 'herramientas', label: 'Kit de Herramientas', hasExpiry: false }, { id: 'repuesto', label: 'Repuesto de Fusibles', hasExpiry: false } ]; // ====================================== // ALMACENAMIENTO // ====================================== async function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => { db = request.result; resolve(db); }; request.onupgradeneeded = (e) => { const db = e.target.result; const stores = ['documents', 'oil', 'fuel', 'checklist', 'config']; stores.forEach(store => { if (!db.objectStoreNames.contains(store)) { db.createObjectStore(store, { keyPath: 'id' }); } }); }; }); } async function saveData(storeName, data) { try { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const request = store.put(data); request.onsuccess = () => { localStorage.setItem(`${storeName}_${data.id}`, JSON.stringify(data)); resolve(request.result); }; request.onerror = () => { localStorage.setItem(`${storeName}_${data.id}`, JSON.stringify(data)); resolve(data.id); }; }); } catch (e) { localStorage.setItem(`${storeName}_${data.id}`, JSON.stringify(data)); return data.id; } } async function getData(storeName, key) { try { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.get(key); request.onsuccess = () => { resolve(request.result || JSON.parse(localStorage.getItem(`${storeName}_${key}`) || 'null')); }; request.onerror = () => { resolve(JSON.parse(localStorage.getItem(`${storeName}_${key}`) || 'null')); }; }); } catch (e) { return JSON.parse(localStorage.getItem(`${storeName}_${key}`) || 'null'); } } async function getAllData(storeName) { try { return new Promise((resolve, reject) => { const transaction = db.transaction([storeName], 'readonly'); const store = transaction.objectStore(storeName); const request = store.getAll(); request.onsuccess = () => resolve(request.result || []); request.onerror = () => resolve([]); }); } catch (e) { const items = []; const prefix = `${storeName}_`; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(prefix)) { items.push(JSON.parse(localStorage.getItem(key))); } } return items; } } // ====================================== // NOTIFICACIONES // ====================================== function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideOut 0.3s ease-out forwards'; setTimeout(() => toast.remove(), 300); }, 3000); } function testNotifications() { showToast('✓ Notificación de prueba', 'success'); showToast('⚠️ Aviso importante', 'warning'); showToast('❌ Error detectado', 'error'); } function requestNotificationPermission() { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } } function sendPushNotification(title, options = {}) { if ('Notification' in window && Notification.permission === 'granted') { new Notification(title, { icon: '🚗', tag: 'miauto', ...options }); } } // ====================================== // DOCUMENTOS // ====================================== async function uploadPDF() { const fileInput = document.getElementById('pdfUpload'); const docType = document.getElementById('docTypeSelect').value; if (!fileInput.files[0]) { showToast('Selecciona un archivo PDF', 'error'); return; } if (!docType) { showToast('Selecciona tipo de documento', 'error'); return; } const file = fileInput.files[0]; const reader = new FileReader(); reader.onload = async (e) => { const base64 = e.target.result; const docData = { id: `doc_${docType}_${Date.now()}`, docType: docType, fileName: file.name, pdfBase64: base64, uploadDate: new Date().toISOString(), expireDate: null, source: 'local' }; await saveData('documents', docData); fileInput.value = ''; document.getElementById('docTypeSelect').value = ''; showToast(`✓ ${DOCS_CONFIG[docType].label} cargado`, 'success'); displayDocuments(); }; reader.readAsDataURL(file); } async function displayDocuments() { const docs = await getAllData('documents'); const today = new Date(); let urgentCount = 0; const html = Object.keys(DOCS_CONFIG).map(docType => { const doc = docs.find(d => d.docType === docType); const config = DOCS_CONFIG[docType]; let statusHtml = '❌ Sin documento'; let docClass = ''; if (doc) { statusHtml = `📤 Cargado: ${new Date(doc.uploadDate).toLocaleDateString('es-CL')}`; if (doc.expireDate) { const expireDate = new Date(doc.expireDate); const daysLeft = Math.ceil((expireDate - today) / (1000 * 60 * 60 * 24)); if (daysLeft < 0) { statusHtml = `⛔ VENCIDO hace ${Math.abs(daysLeft)} días`; docClass = 'urgent'; urgentCount++; } else if (daysLeft < config.alertDays) { statusHtml = `⚠️ Vence en ${daysLeft} días (${expireDate.toLocaleDateString('es-CL')})`; docClass = 'warning'; urgentCount++; } else { statusHtml = `✓ Vigente hasta ${expireDate.toLocaleDateString('es-CL')}`; docClass = 'ok'; } // Lógica especial para permisos con 2 cuotas if (docType === 'permiso' && doc.quote2Date) { const quote2Date = new Date(doc.quote2Date); const daysLeft2 = Math.ceil((quote2Date - today) / (1000 * 60 * 60 * 24)); statusHtml += `
⚠️ Segunda cuota vence en ${daysLeft2} días (${quote2Date.toLocaleDateString('es-CL')})`; } // Recordatorio de revisión técnica por mes if (docType === 'revision' && config.monthlyReminder) { const reviewMonth = expireDate.getMonth(); const currentMonth = today.getMonth(); if (reviewMonth === currentMonth) { statusHtml += `
📅 Este mes toca renovar revisión técnica`; } } } } return `
${config.icon} ${config.label} ${docClass === 'urgent' ? '⛔' : docClass === 'warning' ? '⚠️' : '✓'}
${statusHtml}
${doc ? `
${docType === 'permiso' ? ` ` : ` `}
` : ''}
`; }).join(''); document.getElementById('documentsList').innerHTML = html; // Mostrar alertas if (urgentCount > 0) { const alertaHtml = `
⚠️ Tienes ${urgentCount} documento(s) por vencer o vencido(s)
`; document.getElementById('alertasDocumentos').innerHTML = alertaHtml; document.getElementById('alertasDocumentos').style.display = 'block'; updateTabBadge('auto', urgentCount); } else { document.getElementById('alertasDocumentos').style.display = 'none'; updateTabBadge('auto', 0); } } async function editDocumentDate(docId) { const doc = await getData('documents', docId); const newDate = prompt('Fecha de vencimiento (YYYY-MM-DD):', doc.expireDate || ''); if (newDate) { doc.expireDate = newDate; await saveData('documents', doc); showToast('✓ Fecha actualizada', 'success'); displayDocuments(); } } async function editDocumentQuotas(docId) { const doc = await getData('documents', docId); const quote1 = prompt('Fecha vencimiento cuota 1 (YYYY-MM-DD):', doc.expireDate || ''); if (quote1) { const quote2 = prompt('Fecha vencimiento cuota 2 (YYYY-MM-DD):', doc.quote2Date || ''); if (quote2) { doc.expireDate = quote1; doc.quote2Date = quote2; await saveData('documents', doc); showToast('✓ Cuotas actualizadas', 'success'); displayDocuments(); } } } // ====================================== // VISOR PDF MEJORADO // ====================================== let currentPdfDoc = null; let currentZoom = 100; function viewPDF(docId) { getData('documents', docId).then(doc => { if (doc && doc.pdfBase64) { currentPdfDoc = doc; currentZoom = 100; document.getElementById('pdfModalTitle').textContent = DOCS_CONFIG[doc.docType].label; document.getElementById('pdfViewer').src = doc.pdfBase64; document.getElementById('zoomLevel').textContent = '100%'; document.getElementById('pdfModal').classList.add('active'); } }); } function closePDFModal() { document.getElementById('pdfModal').classList.remove('active'); document.getElementById('pdfViewer').src = ''; currentPdfDoc = null; } function zoomPDF(amount) { currentZoom = Math.max(50, Math.min(300, currentZoom + amount)); const viewer = document.getElementById('pdfViewer'); viewer.style.transform = `scale(${currentZoom / 100})`; viewer.style.transformOrigin = 'top center'; document.getElementById('zoomLevel').textContent = `${currentZoom}%`; } function resetZoomPDF() { currentZoom = 100; const viewer = document.getElementById('pdfViewer'); viewer.style.transform = 'scale(1)'; document.getElementById('zoomLevel').textContent = '100%'; } function downloadPDF() { if (currentPdfDoc && currentPdfDoc.pdfBase64) { const link = document.createElement('a'); link.href = currentPdfDoc.pdfBase64; link.download = currentPdfDoc.fileName || `${currentPdfDoc.docType}.pdf`; link.click(); showToast('✓ PDF descargado', 'success'); } } function fullscreenPDF() { const container = document.getElementById('pdfViewerContainer'); if (container.requestFullscreen) { container.requestFullscreen().catch(err => { showToast('No se pudo activar pantalla completa', 'error'); }); } else if (container.webkitRequestFullscreen) { container.webkitRequestFullscreen(); } } async function deleteDocument(docId) { if (confirm('¿Eliminar este documento?')) { // Eliminar de IndexedDB const transaction = db.transaction(['documents'], 'readwrite'); const store = transaction.objectStore('documents'); store.delete(docId); // Eliminar de localStorage localStorage.removeItem(`documents_${docId}`); showToast('✓ Documento eliminado', 'success'); displayDocuments(); } } // ====================================== // ACEITE // ====================================== async function recordOilChange() { const interval = parseInt(document.getElementById('oilInterval').value); const currentKm = parseInt(document.getElementById('currentKmAceite').value); const brand = document.getElementById('oilBrand').value; const oilRecord = { id: `oil_${Date.now()}`, km: currentKm, brand: brand, date: new Date().toISOString(), nextChangeKm: currentKm + interval }; await saveData('oil', oilRecord); showToast('✓ Cambio de aceite registrado', 'success'); displayOilStatus(); } async function displayOilStatus() { const currentKm = parseInt(document.getElementById('currentKmAceite').value) || 0; const interval = parseInt(document.getElementById('oilInterval').value); const oilRecords = await getAllData('oil'); const lastChange = oilRecords.length > 0 ? oilRecords[oilRecords.length - 1] : null; const nextChangeKm = lastChange ? lastChange.nextChangeKm : currentKm + interval; const kmUntilChange = nextChangeKm - currentKm; const percentUsed = lastChange ? ((currentKm - lastChange.km) / interval * 100) : 0; const statsHtml = `
Próximo cambio
${nextChangeKm} km
Faltan ${Math.max(0, kmUntilChange)} km
Ciclo usado
${percentUsed.toFixed(1)}%
${lastChange ? currentKm - lastChange.km : 0} km
`; document.getElementById('oilStats').innerHTML = statsHtml; document.getElementById('oilStats').style.display = 'grid'; if (oilRecords.length > 0) { const historyHtml = `

📜 Historial:

${oilRecords.map(r => `
${new Date(r.date).toLocaleDateString('es-CL')}
Próximo cambio: ${r.nextChangeKm} km
`).join('')} `; document.getElementById('oilHistory').innerHTML = historyHtml; } // Alertar si está cerca del cambio if (percentUsed > 85) { sendPushNotification('⚠️ Cambio de aceite próximo', { body: `Tu auto necesitará cambio de aceite en ${Math.max(0, kmUntilChange)} km` }); } } // ====================================== // COMBUSTIBLE // ====================================== async function recordFuelCharge() { const km = parseInt(document.getElementById('fuelKm').value); const liters = parseFloat(document.getElementById('fuelLiters').value); const price = parseInt(document.getElementById('fuelPrice').value); const month = document.getElementById('fuelMonth').value; if (!km || !liters || !month) { showToast('Completa todos los campos', 'error'); return; } const fuelRecord = { id: `fuel_${Date.now()}`, km: km, liters: liters, price: price, month: month, cost: liters * price, date: new Date().toISOString() }; await saveData('fuel', fuelRecord); document.getElementById('fuelKm').value = ''; document.getElementById('fuelLiters').value = ''; document.getElementById('fuelMonth').value = ''; showToast('✓ Carga registrada', 'success'); displayFuelStatus(); } async function displayFuelStatus() { const fuelRecords = await getAllData('fuel'); const currentMonth = document.getElementById('fuelMonth').value || new Date().toISOString().slice(0, 7); const monthRecords = fuelRecords.filter(r => r.month === currentMonth); const totalCargas = monthRecords.length; const totalLiters = monthRecords.reduce((sum, r) => sum + r.liters, 0); const totalCost = monthRecords.reduce((sum, r) => sum + r.cost, 0); let kmPerLiter = 0; if (monthRecords.length > 1) { const kmDiff = Math.max(...monthRecords.map(r => r.km)) - Math.min(...monthRecords.map(r => r.km)); kmPerLiter = kmDiff / totalLiters; } const statsHtml = `
Cargas este mes
${totalCargas}
Promedio km/L
${kmPerLiter.toFixed(2)}
Bencina consumida
${totalLiters.toFixed(1)} L
Total gastado
$${totalCost.toFixed(0)}
`; document.getElementById('fuelMonthStats').innerHTML = statsHtml; document.getElementById('fuelMonthStats').style.display = 'grid'; if (monthRecords.length > 0) { const sorted = [...monthRecords].sort((a, b) => b.km - a.km); const historyHtml = `

⛽ Cargas de ${currentMonth}:

${sorted.map(r => `
$${r.cost.toFixed(0)}
${new Date(r.date).toLocaleDateString('es-CL')} a ${r.price}$/L
`).join('')} `; document.getElementById('fuelHistory').innerHTML = historyHtml; } } // ====================================== // CHECKLIST // ====================================== async function displayChecklist() { const checklistData = await getAllData('checklist'); const today = new Date(); let urgentCount = 0; const html = CHECKLIST_ITEMS.map(item => { const saved = checklistData.find(c => c.id === item.id); const isChecked = saved ? saved.completed : false; const expireDate = saved ? saved.expireDate : null; let classList = 'checklist-item'; let expiryInfo = ''; if (isChecked) { classList += ' completed'; } if (item.hasExpiry && expireDate) { const expire = new Date(expireDate); const daysLeft = Math.ceil((expire - today) / (1000 * 60 * 60 * 24)); if (daysLeft < 0) { classList += ' urgent'; expiryInfo = `Vencido hace ${Math.abs(daysLeft)} días`; urgentCount++; } else if (daysLeft < 30) { classList += ' urgent'; expiryInfo = `Vence en ${daysLeft} días`; urgentCount++; } else { expiryInfo = `Vence en ${daysLeft} días`; } } return `
${item.hasExpiry ? ` ${expiryInfo ? `
${expiryInfo}
` : ''} ` : ''}
`; }).join(''); document.getElementById('checklistItems').innerHTML = html; updateTabBadge('checklist', urgentCount); } async function updateChecklistItem(id, completed) { let item = await getData('checklist', id) || { id: id }; item.completed = completed; item.date = new Date().toISOString(); await saveData('checklist', item); displayChecklist(); } async function updateChecklistDate(id, date) { let item = await getData('checklist', id) || { id: id }; item.expireDate = date; await saveData('checklist', item); if (date) { const expire = new Date(date); const today = new Date(); const daysLeft = Math.ceil((expire - today) / (1000 * 60 * 60 * 24)); if (daysLeft < 30 && daysLeft > 0) { sendPushNotification('⚠️ Elemento del checklist por vencer', { body: `${CHECKLIST_ITEMS.find(i => i.id === id).label} vence en ${daysLeft} días` }); } } displayChecklist(); } async function resetChecklist() { if (confirm('¿Limpiar el checklist?')) { const items = CHECKLIST_ITEMS.map(item => item.id); for (const id of items) { localStorage.removeItem(`checklist_${id}`); } const transaction = db.transaction(['checklist'], 'readwrite'); transaction.objectStore('checklist').clear(); displayChecklist(); showToast('✓ Checklist limpiado', 'success'); } } // ====================================== // GOOGLE DRIVE // ====================================== function toggleGDriveSetup() { document.getElementById('gdriveSetupModal').classList.toggle('active'); } function closeGDriveSetup() { document.getElementById('gdriveSetupModal').classList.remove('active'); } function connectGDrive() { alert('Para conectar Google Drive:\n\n1. Obtén API Key en Google Cloud Console\n2. Habilita Google Drive API\n3. Pega tu API Key en la configuración\n\nPor ahora, usa upload local de PDFs.'); } function pickFromGDrive() { alert('Seleccionar archivos de Google Drive requiere backend OAuth2.\n\nPor ahora, descarga PDFs y cárgalos localmente.'); } function saveGDriveConfig() { const apiKey = document.getElementById('gdriveApiKey').value; if (apiKey) { localStorage.setItem('gdriveApiKey', apiKey); showToast('✓ Configuración guardada', 'success'); closeGDriveSetup(); } } // ====================================== // UTILIDADES // ====================================== function updateTabBadge(tabName, count) { const badgeId = `badge${tabName.charAt(0).toUpperCase() + tabName.slice(1)}`; const badge = document.getElementById(badgeId); if (badge && count > 0) { badge.innerHTML = `${count}`; } else if (badge) { badge.innerHTML = ''; } } function switchTab(tabName) { document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.getElementById(tabName).classList.add('active'); event.target.classList.add('active'); if (tabName === 'auto') displayDocuments(); if (tabName === 'aceite') displayOilStatus(); if (tabName === 'combustible') displayFuelStatus(); if (tabName === 'checklist') displayChecklist(); } // ====================================== // INICIALIZACIÓN // ====================================== async function initApp() { try { await initDB(); await initActivationScreen(); // Verificar código de activación primero requestNotificationPermission(); updateAppStatus(); // Cargar mes actual const today = new Date(); document.getElementById('fuelMonth').value = today.toISOString().slice(0, 7); // Cargar datos iniciales displayDocuments(); displayOilStatus(); displayFuelStatus(); displayChecklist(); // Registrar Service Worker robusto if ('serviceWorker' in navigator) { const swCode = ` const CACHE_NAME = 'miauto-v4-1'; const urlsToCache = ['/']; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return cache.addAll(urlsToCache).catch(() => true); }) ); self.skipWaiting(); }); self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { return caches.delete(cacheName); } }) ); }) ); self.clients.claim(); }); self.addEventListener('fetch', event => { if (event.request.method !== 'GET') { return; } event.respondWith( caches.match(event.request).then(response => { if (response) { return response; } return fetch(event.request).then(response => { if (!response || response.status !== 200 || response.type === 'error') { return response; } const responseToCache = response.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return response; }).catch(() => { return caches.match(event.request) || new Response('Offline - Funcionalidad limitada'); }); }) ); }); `; navigator.serviceWorker.register('data:application/javascript;base64,' + btoa(swCode)).catch(err => console.log('SW error:', err)); } showToast('✓ MiAutoPro v4.1 cargada', 'success'); } catch (e) { console.error('Error:', e); showToast('Error al inicializar la app', 'error'); } } document.addEventListener('DOMContentLoaded', initApp); document.getElementById('currentKmAceite').addEventListener('change', displayOilStatus); document.getElementById('fuelMonth').addEventListener('change', displayFuelStatus);