diff --git a/index.html b/index.html index fd09ca4..d2ea136 100644 --- a/index.html +++ b/index.html @@ -19,7 +19,7 @@
Nenhum XML carregado.- +
A operação só marca avaria e falta. Aqui no ADM você copia as linhas prontas para Excel.
+A operação só marca avaria e falta. Aqui no CCO você copia as linhas prontas para Excel.
Transportadora: ${nota.transportadora || '-'}
Motorista: ${nota.motorista}
Telefone: ${nota.telefone_motorista}
Placa: ${nota.placa}
DT/Remessa: ${nota.dt_remessa || '-'}
${nota.reaberta ? 'Carga reaberta pelo ADM - refaça a conferência.
' : ''}`; + notaInfo.innerHTML = `Transportadora: ${nota.transportadora || '-'}
Motorista: ${nota.motorista}
Telefone: ${nota.telefone_motorista}
Placa: ${nota.placa}
DT/Remessa: ${nota.dt_remessa || '-'}
${nota.reaberta ? 'Carga reaberta pelo CCO - refaça a conferência.
' : ''}`; conferenciaForm.innerHTML = `${code} - ${item.descricao}
${fardosPorPalete} fardos por palete
Avaria: ${conf.avaria ? 'Sim' : 'Não'} | Falta: ${conf.faltando ? 'Sim' : 'Não'}
PL2: ${conf.pl2 ? 'Sim' : 'Não'} | Paletes: ${conf.paletes_total || '-'}
Início carga: ${conf.inicio_carga ? new Date(conf.inicio_carga).toLocaleString('pt-BR') : '-'} | Fim: ${conf.fim_carga ? new Date(conf.fim_carga).toLocaleString('pt-BR') : '-'}
+Assinatura: ${conf.assinatura_em ? `Sim (${new Date(conf.assinatura_em).toLocaleString('pt-BR')})` : 'Não'}
Observação: ${conf.observacao || '-'}
Divergências: ${divergencias.map((d) => `${d.codigo} (${d.divergencia})`).join(', ') || 'Nenhuma'}
${nota?.anotacao_reabertura ? `Reabertura: ${nota.anotacao_reabertura}
` : ''} @@ -614,6 +698,7 @@ async function loadConferencias() { document.querySelectorAll('[data-nota-reopen]').forEach((btn) => { btn.addEventListener('click', () => reabrirConferencia(Number(btn.dataset.notaReopen))); }); + return data || []; } async function loadNqReports() { @@ -639,46 +724,99 @@ async function loadNqReports() { }); } -function loadDescargaPlanilha() { +async function generateSignedReceipt(notaId) { + const { jsPDF } = window.jspdf || {}; + if (!jsPDF) return alert('Biblioteca de PDF não carregada.'); + const nota = state.invoices.find((n) => String(n.id) === String(notaId)); + const conferencias = await dbSelect('conferencias', { eq: { nota_id: notaId } }) || []; + const conf = conferencias.at(-1); + if (!nota || !conf?.assinatura_data_url) return alert('Assinatura não encontrada para esta nota.'); + + const pdf = new jsPDF('p', 'mm', 'a4'); + pdf.setFont('helvetica', 'bold'); + pdf.setFontSize(15); + pdf.text(`Nota assinada - NF ${nota.numero_nota}`, 14, 18); + pdf.setFont('helvetica', 'normal'); + pdf.setFontSize(11); + pdf.text(`Motorista: ${nota.motorista || '-'}`, 14, 28); + pdf.text(`Placa: ${nota.placa || '-'} Remessa: ${nota.dt_remessa || '-'}`, 14, 35); + pdf.text(`Data impressão: ${new Date().toLocaleString('pt-BR')}`, 14, 42); + pdf.text(`Assinatura conferente: ${new Date(conf.assinatura_em).toLocaleString('pt-BR')}`, 14, 49); + let y = 60; + pdf.setFont('helvetica', 'bold'); + pdf.text('SKU', 14, y); + pdf.text('Qtd NF', 70, y); + pdf.text('Qtd Conferida', 100, y); + pdf.text('Dif.', 145, y); + y += 6; + pdf.setFont('helvetica', 'normal'); + (conf.itens_conferidos || []).forEach((item) => { + pdf.text(String(item.codigo || '-'), 14, y); + pdf.text(String(item.quantidadeFardo ?? 0), 70, y); + pdf.text(String(item.conferido ?? 0), 100, y); + pdf.text(String(item.divergencia ?? 0), 145, y); + y += 6; + if (y > 240) { + pdf.addPage(); + y = 20; + } + }); + pdf.setFont('helvetica', 'bold'); + pdf.text('Assinatura digital', 14, y + 8); + pdf.addImage(conf.assinatura_data_url, 'PNG', 14, y + 12, 90, 35); + pdf.save(`nota-assinada-${nota.numero_nota}.pdf`); +} + +function loadDescargaPlanilha(conferencias = []) { if (!adminDescargasContainer) return; - const rows = [...state.invoices] - .sort((a, b) => new Date(a.created_at || 0) - new Date(b.created_at || 0)) - .map((nota) => ` -| DT | -Placa | -Motorista | -NF | -Início carga | -Término | -PL2 | -Paletes | -
|---|
Nenhuma nota pendente.
'} +Nenhuma nota assinada.
'} +Sem notas publicadas.
'; +} + function buildRecebimentoLine(nota, extra = {}) { const referencia = nota?.xml_raw ? parseXmlText(nota.xml_raw) : null; const chegada = new Date(nota?.created_at || Date.now()); @@ -760,7 +898,7 @@ function formatFileTimestamp(date = new Date()) { } async function guardarLogsNoTerabox() { - if (!state.user || state.user.role !== 'adm') return alert('Apenas ADM pode guardar logs no TeraBox.'); + if (!state.user || state.user.role !== 'adm') return alert('Apenas CCO pode guardar logs no TeraBox.'); if (!state.logs.length) return alert('Sem logs para guardar.'); const header = 'DataHora\tUsuario\tTipo\tCodigo\tQtdDivergencia\tMensagem'; const lines = state.logs.map((log) => [ @@ -868,6 +1006,15 @@ async function clearLogs() { } } +async function clearHistoricoCargas() { + if (!confirm('Deseja apagar do Supabase/local todo o histórico de cargas (notas, conferências e NQ)?')) return; + await dbDeleteAll('conferencias'); + await dbDeleteAll('nq_reports'); + await dbDeleteAll('notas'); + await logAction('HISTORICO_CARGAS_LIMPO', null, 0, 'Histórico de cargas apagado pelo CCO', false); + await refreshAll(); +} + function toggleChat() { qs('chatPanel').classList.toggle('hidden'); } async function sendChat(evt) { @@ -897,10 +1044,11 @@ async function refreshAll() { await loadInvoices(); if (state.user?.role === 'adm') { await loadLogs(); - await loadConferencias(); + const conferencias = await loadConferencias(); await loadNqReports(); - loadDescargaPlanilha(); + loadDescargaPlanilha(conferencias || []); loadRecebimentoTab(); + loadAdminNotasResumo(); } await loadChats(); } @@ -918,6 +1066,9 @@ qs('btnExportLogs').addEventListener('click', exportLogs); qs('btnClearLogs').addEventListener('click', clearLogs); qs('btnLogout').addEventListener('click', () => setUser(null)); qs('btnPrintInvoice')?.addEventListener('click', generatePdfFromPages); +qs('btnClearXml')?.addEventListener('click', clearXmlPreview); +qs('btnClearHistorico')?.addEventListener('click', clearHistoricoCargas); +qs('btnRefreshDashboard')?.addEventListener('click', refreshAll); qs('chatBubble').addEventListener('click', toggleChat); qs('chatForm').addEventListener('submit', sendChat); qs('btnLogs').addEventListener('click', () => setLogsPage(true)); @@ -957,11 +1108,11 @@ setInterval(() => { if (!state.user) return; loadChats(); if (state.user.role === 'adm') { - loadConferencias(); + loadConferencias().then((conferencias) => loadDescargaPlanilha(conferencias || [])); loadLogs(); loadNqReports(); - loadDescargaPlanilha(); loadRecebimentoTab(); + loadAdminNotasResumo(); } }, 6000); diff --git a/styles.css b/styles.css index 335fb72..fa88145 100644 --- a/styles.css +++ b/styles.css @@ -11,6 +11,7 @@ label { font-size: 14px; display: flex; flex-direction: column; gap: 4px; } input, select, textarea, button { padding: 8px; border-radius: 8px; border: 1px solid #c4c9d4; } button { cursor: pointer; border: none; background: #2766cf; color: white; } button.danger { background: #ca2e2e; } +button.ghost { background: #eef2fa; color: #1d3155; border: 1px solid #b9c6df; } .toolbar { display: flex; gap: 8px; align-items: center; } .hidden { display: none !important; } .preview { max-height: 320px; overflow: auto; padding: 10px; background: #eff3fb; border-radius: 8px; } @@ -18,6 +19,10 @@ button.danger { background: #ca2e2e; } .divergente { border-color: #e54343; background: #fff0f0; } .faltando { border-color: #d7a700; background: #fff8d9; } .item { border: 1px solid #e2e6ef; border-radius: 8px; padding: 10px; } +.signature-box { border: 1px dashed #9fb2d8; border-radius: 8px; padding: 8px; background: #f8fbff; } +#signatureCanvas { width: 100%; max-width: 520px; border: 1px solid #d5dff0; border-radius: 6px; background: #fff; touch-action: none; } +table { width: 100%; border-collapse: collapse; } +th, td { border: 1px solid #dde3ee; padding: 6px; text-align: left; font-size: 13px; } .tabs { display: flex; gap: 8px; margin: 8px 0 14px; flex-wrap: wrap; } .tab-btn { background: #e9eef8; color: #203254; border: 1px solid #b8c8e8; } diff --git a/supabase-schema.sql b/supabase-schema.sql index 2eca649..fb03cae 100644 --- a/supabase-schema.sql +++ b/supabase-schema.sql @@ -31,6 +31,7 @@ create table if not exists public.notas ( reaberta_em timestamptz, anotacao_reabertura text, descarga_fechada boolean default false, + ultima_impressao_em timestamptz, publicado_por text not null, created_at timestamptz default now() ); @@ -49,6 +50,8 @@ create table if not exists public.conferencias ( fim_carga timestamptz, reabertura_finalizada boolean default false, avaria_obs text, + assinatura_data_url text, + assinatura_em timestamptz, itens_conferidos jsonb not null, created_at timestamptz default now() ); @@ -99,6 +102,7 @@ alter table public.notas add column if not exists paletes_total numeric; alter table public.notas add column if not exists reaberta boolean default false; alter table public.notas add column if not exists reaberta_em timestamptz; alter table public.notas add column if not exists anotacao_reabertura text; +alter table public.notas add column if not exists ultima_impressao_em timestamptz; alter table public.conferencias add column if not exists avaria boolean default false; alter table public.conferencias add column if not exists faltando boolean default false; alter table public.conferencias add column if not exists avaria_obs text; @@ -107,3 +111,5 @@ alter table public.conferencias add column if not exists paletes_total numeric; alter table public.conferencias add column if not exists inicio_carga timestamptz; alter table public.conferencias add column if not exists fim_carga timestamptz; alter table public.conferencias add column if not exists reabertura_finalizada boolean default false; +alter table public.conferencias add column if not exists assinatura_data_url text; +alter table public.conferencias add column if not exists assinatura_em timestamptz;