3e64514b08
Financeiro V1 (Épicos 1–4): - FKs de origem no lançamento (paciente/profissional/procedimento/agendamento/contrato/realizado), soft delete e cron "Atrasado" - prontuário de procedimentos realizados (fotos antes/depois) + regra sem-comissão (garantia/reabertura do mesmo profissional) - normalizeFinanceiro: corrige CHECK capitalizado vs enforceUppercase (lançamentos não salvavam pela UI) - baixa de pagamento (pago_em), despesas (fornecedor/centro de custo/recorrente), competência por data de conclusão - relatório financeiro (período, paciente, profissional, forma, convênio, glosa) - orçamento Assinado -> contas a receber; contrato vigente -> cobrança recorrente; renovação automática de contrato Financeiro V2 (Comissão/Repasse): - regras de % (padrão/procedimento/override por profissional) + custo de laboratório - base selecionável (recebido/produção), fechamento persistido, pagar (gera despesa) + comprovante + estorno Glosas de convênio: por item de GTO, recurso/protocolo/prazo, recuperar/estornar, impacto no relatório GTO -> Financeiro: finalizar gera RECEITA (convênio a receber); LancarGTO passa a persistir; convênios reais (planos) Contratos: modelo "Atendimento a Convênios / Repasse (Dentista Credenciado)" + variáveis de convênio Salas (redesenho): perfil "Dono de Sala"; locação sempre ativa; menus MINHAS/ALUGAR SALAS; sala como workspace isolado (seletor agrupado Clínicas x Salas, tenantGuard por dono); financeiro de locação (reserva confirmada -> recebível); Painel da Sala (KPIs, agenda visual, recebíveis, relatório por sala) Docs: BACKLOG-FINANCEIRO-V1, AUDITORIA-FUNCIONAL-SCOREODONTO, CHECKLIST-DEPLOY-PRODUCAO Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
411 lines
17 KiB
TypeScript
411 lines
17 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { FileDown, Loader2, Camera, Activity, GraduationCap } from 'lucide-react';
|
|
import { TratamentoOrto } from '../types.ts';
|
|
import { HybridBackend } from '../services/backend.ts';
|
|
import { useToast } from '../contexts/ToastContext.tsx';
|
|
import { ORTO_SLOTS } from './OrthoPhotoWorkspace.tsx';
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────
|
|
|
|
// Converte URL de imagem para base64 DataURL para uso no jsPDF
|
|
async function imgToDataUrl(url: string): Promise<string | null> {
|
|
try {
|
|
const res = await fetch(url);
|
|
const blob = await res.blob();
|
|
return await new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result as string);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
} catch { return null; }
|
|
}
|
|
|
|
// Quebra texto em linhas para jsPDF
|
|
function splitText(text: string, maxW: number, charPerW = 2.5): string[] {
|
|
if (!text) return [];
|
|
const words = text.split(' ');
|
|
const maxChars = Math.floor(maxW * charPerW);
|
|
const lines: string[] = [];
|
|
let line = '';
|
|
for (const w of words) {
|
|
if ((line + ' ' + w).length > maxChars && line) { lines.push(line); line = w; }
|
|
else line = line ? `${line} ${w}` : w;
|
|
}
|
|
if (line) lines.push(line);
|
|
return lines;
|
|
}
|
|
|
|
// ─── PDF 1: Documentação Fotográfica ─────────────────────────────
|
|
|
|
async function gerarPDFDocumentacao(t: TratamentoOrto) {
|
|
const { default: jsPDF } = await import('jspdf');
|
|
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
|
const W = 210, H = 297, M = 12;
|
|
let y = M;
|
|
|
|
const accent = [13, 148, 136]; // teal-600 (paleta do login)
|
|
|
|
// ── Cabeçalho ──
|
|
doc.setFillColor(...accent as [number, number, number]);
|
|
doc.rect(0, 0, W, 22, 'F');
|
|
doc.setTextColor(255, 255, 255);
|
|
doc.setFontSize(13); doc.setFont('helvetica', 'bold');
|
|
doc.text('DOCUMENTAÇÃO FOTOGRÁFICA ORTODÔNTICA', M, 9);
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal');
|
|
doc.text('SCOREODONTO.COM', M, 15);
|
|
doc.text(`Gerado em ${new Date().toLocaleString('pt-BR')}`, W - M, 15, { align: 'right' });
|
|
y = 28;
|
|
|
|
// ── Dados do paciente ──
|
|
doc.setTextColor(30, 30, 30);
|
|
doc.setFontSize(11); doc.setFont('helvetica', 'bold');
|
|
doc.text(t.pacienteNome.toUpperCase(), M, y); y += 5;
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal');
|
|
doc.setTextColor(100);
|
|
const info = [
|
|
`Aparelho: ${t.tipoAparelho || '—'}`,
|
|
`Início: ${new Date(t.dataInicio).toLocaleDateString('pt-BR')}`,
|
|
`Fase: ${t.faseAtual || '—'}`,
|
|
`Progresso: ${t.percentualConcluido ?? '—'}%`,
|
|
`Status: ${t.status}`,
|
|
].join(' | ');
|
|
doc.text(info, M, y); y += 4;
|
|
doc.setDrawColor(...accent as [number, number, number]);
|
|
doc.setLineWidth(0.3);
|
|
doc.line(M, y, W - M, y); y += 6;
|
|
|
|
// ── Fotos em grade ──
|
|
const sessoes = await HybridBackend.getOrtoSessoes(t.id);
|
|
const allFotos: Record<string, string> = {};
|
|
for (const s of (sessoes.sessoes || [])) {
|
|
for (const f of (s.fotos || [])) allFotos[f.slot] = f.url;
|
|
}
|
|
|
|
const cats = ['EXTRAORAL', 'INTRAORAL', 'COMPLEMENTAR'];
|
|
const COLS = 5;
|
|
const cellW = (W - M * 2 - (COLS - 1) * 3) / COLS;
|
|
const cellH = cellW * 0.75;
|
|
|
|
for (const cat of cats) {
|
|
const slots = ORTO_SLOTS.filter(s => s.cat === cat);
|
|
doc.setFontSize(7); doc.setFont('helvetica', 'bold');
|
|
doc.setTextColor(...accent as [number, number, number]);
|
|
doc.text(cat, M, y); y += 4;
|
|
|
|
let col = 0;
|
|
let rowY = y;
|
|
for (const slot of slots) {
|
|
const x = M + col * (cellW + 3);
|
|
const url = allFotos[slot.key];
|
|
if (url) {
|
|
const dataUrl = await imgToDataUrl(url);
|
|
if (dataUrl) {
|
|
doc.addImage(dataUrl, 'JPEG', x, rowY, cellW, cellH);
|
|
}
|
|
} else {
|
|
doc.setFillColor(240, 240, 240);
|
|
doc.roundedRect(x, rowY, cellW, cellH, 1, 1, 'F');
|
|
doc.setTextColor(180); doc.setFontSize(5.5);
|
|
doc.text('SEM FOTO', x + cellW / 2, rowY + cellH / 2, { align: 'center' });
|
|
}
|
|
// Label
|
|
doc.setFontSize(5); doc.setFont('helvetica', 'normal'); doc.setTextColor(80);
|
|
doc.text(slot.label, x + cellW / 2, rowY + cellH + 2.5, { align: 'center' });
|
|
col++;
|
|
if (col >= COLS) { col = 0; rowY += cellH + 8; }
|
|
}
|
|
y = rowY + (col > 0 ? cellH + 8 : 0) + 4;
|
|
|
|
if (y > H - 30) { doc.addPage(); y = M; }
|
|
}
|
|
|
|
// ── Rodapé ──
|
|
doc.setTextColor(150); doc.setFontSize(6);
|
|
doc.text(`SCOREODONTO.COM · ${t.pacienteNome} · Documentação Fotográfica`, W / 2, H - 6, { align: 'center' });
|
|
|
|
doc.save(`FOTO_${t.pacienteNome.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`);
|
|
}
|
|
|
|
// ─── PDF 2: Evolução Ortodôntica ──────────────────────────────────
|
|
|
|
async function gerarPDFEvolucao(t: TratamentoOrto) {
|
|
const { default: jsPDF } = await import('jspdf');
|
|
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
|
const W = 210, H = 297, M = 14;
|
|
let y = M;
|
|
|
|
const accent = [5, 150, 105]; // emerald-600
|
|
|
|
doc.setFillColor(...accent as [number, number, number]);
|
|
doc.rect(0, 0, W, 22, 'F');
|
|
doc.setTextColor(255); doc.setFontSize(13); doc.setFont('helvetica', 'bold');
|
|
doc.text('RELATÓRIO DE EVOLUÇÃO ORTODÔNTICA', M, 9);
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal');
|
|
doc.text('SCOREODONTO.COM', M, 15);
|
|
doc.text(`Gerado em ${new Date().toLocaleString('pt-BR')}`, W - M, 15, { align: 'right' });
|
|
y = 28;
|
|
|
|
doc.setTextColor(30); doc.setFontSize(12); doc.setFont('helvetica', 'bold');
|
|
doc.text(t.pacienteNome, M, y); y += 5;
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(90);
|
|
doc.text(`${t.tipoAparelho || '—'} · Início: ${new Date(t.dataInicio).toLocaleDateString('pt-BR')} · ${t.percentualConcluido ?? '—'}% concluído · ${t.evolucoes.length} atendimento(s)`, M, y); y += 5;
|
|
|
|
// Stats boxes
|
|
const stats = [
|
|
{ l: 'ATENDIMENTOS', v: String(t.evolucoes.length) },
|
|
{ l: 'FASE ATUAL', v: t.faseAtual || '—' },
|
|
{ l: 'FIO ATUAL', v: t.fioAtual || '—' },
|
|
{ l: 'MESES REST.', v: t.mesesRestantes ? `${t.mesesRestantes}m` : '—' },
|
|
];
|
|
const bw = (W - M * 2 - 9) / 4;
|
|
stats.forEach((s, i) => {
|
|
const bx = M + i * (bw + 3);
|
|
doc.setFillColor(240, 253, 244);
|
|
doc.roundedRect(bx, y, bw, 12, 1, 1, 'F');
|
|
doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.setTextColor(...accent as [number, number, number]);
|
|
doc.text(s.v, bx + bw / 2, y + 6, { align: 'center' });
|
|
doc.setFontSize(5.5); doc.setFont('helvetica', 'normal'); doc.setTextColor(120);
|
|
doc.text(s.l, bx + bw / 2, y + 10.5, { align: 'center' });
|
|
});
|
|
y += 17;
|
|
|
|
doc.setDrawColor(...accent as [number, number, number]); doc.setLineWidth(0.3);
|
|
doc.line(M, y, W - M, y); y += 5;
|
|
|
|
// Timeline
|
|
const sorted = [...t.evolucoes].sort((a, b) => new Date(a.data).getTime() - new Date(b.data).getTime());
|
|
for (const [i, ev] of sorted.entries()) {
|
|
if (y > H - 25) { doc.addPage(); y = M; }
|
|
|
|
// Número + data
|
|
doc.setFillColor(...accent as [number, number, number]);
|
|
doc.circle(M + 2.5, y + 2.5, 2.5, 'F');
|
|
doc.setTextColor(255); doc.setFontSize(5.5); doc.setFont('helvetica', 'bold');
|
|
doc.text(String(i + 1), M + 2.5, y + 3.2, { align: 'center' });
|
|
|
|
const ex = M + 8;
|
|
doc.setTextColor(30); doc.setFontSize(8); doc.setFont('helvetica', 'bold');
|
|
doc.text(`${new Date(ev.data).toLocaleDateString('pt-BR')} — ${ev.tipo}`, ex, y + 3);
|
|
y += 5;
|
|
|
|
doc.setFontSize(7); doc.setFont('helvetica', 'normal'); doc.setTextColor(60);
|
|
doc.text(ev.procedimento, ex, y + 2);
|
|
y += 5;
|
|
|
|
const tags: string[] = [];
|
|
if (ev.fio) tags.push(`Fio: ${ev.fio}`);
|
|
if (ev.elastico) tags.push(`Elástico: ${ev.elastico}`);
|
|
if (ev.higiene != null) tags.push(`Higiene: ${ev.higiene}/10`);
|
|
if (ev.cooperacao) tags.push(`Cooperação: ${ev.cooperacao}`);
|
|
if (ev.procedimentos?.length) tags.push(...ev.procedimentos);
|
|
if (tags.length) {
|
|
doc.setFontSize(6.5); doc.setTextColor(80);
|
|
doc.text(tags.join(' · '), ex, y + 2);
|
|
y += 4;
|
|
}
|
|
if (ev.observacoes) {
|
|
doc.setFontSize(6); doc.setTextColor(120); doc.setFont('helvetica', 'italic');
|
|
doc.text(`Obs: ${ev.observacoes}`, ex, y + 2);
|
|
doc.setFont('helvetica', 'normal');
|
|
y += 4;
|
|
}
|
|
doc.setTextColor(180); doc.setFontSize(6);
|
|
doc.text(`Próx. retorno: ${new Date(ev.proximoRetorno).toLocaleDateString('pt-BR')}`, ex, y + 2);
|
|
y += 6;
|
|
|
|
if (i < sorted.length - 1) {
|
|
doc.setDrawColor(220); doc.setLineWidth(0.1);
|
|
doc.line(M, y, W - M, y);
|
|
y += 4;
|
|
}
|
|
}
|
|
|
|
doc.setTextColor(150); doc.setFontSize(6);
|
|
doc.text(`SCOREODONTO.COM · ${t.pacienteNome} · Evolução Ortodôntica`, W / 2, H - 6, { align: 'center' });
|
|
doc.save(`EVOLUCAO_${t.pacienteNome.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`);
|
|
}
|
|
|
|
// ─── PDF 3: Caso Clínico para Tutoria ────────────────────────────
|
|
|
|
async function gerarPDFCasoClinico(t: TratamentoOrto) {
|
|
const { default: jsPDF } = await import('jspdf');
|
|
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
|
const W = 210, H = 297, M = 14;
|
|
let y = M;
|
|
|
|
const accent = [13, 148, 136]; // teal-600 (paleta do login)
|
|
|
|
doc.setFillColor(...accent as [number, number, number]);
|
|
doc.rect(0, 0, W, 22, 'F');
|
|
doc.setTextColor(255); doc.setFontSize(13); doc.setFont('helvetica', 'bold');
|
|
doc.text('CASO CLÍNICO PARA TUTORIA ORTODÔNTICA', M, 9);
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal');
|
|
doc.text('SCOREODONTO.COM · CONFIDENCIAL', M, 15);
|
|
doc.text(`Gerado em ${new Date().toLocaleString('pt-BR')}`, W - M, 15, { align: 'right' });
|
|
y = 28;
|
|
|
|
const field = (label: string, value: string | undefined | null) => {
|
|
if (!value) return;
|
|
doc.setFontSize(7); doc.setFont('helvetica', 'bold'); doc.setTextColor(120);
|
|
doc.text(label.toUpperCase(), M, y); y += 3.5;
|
|
doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(30);
|
|
const lines = splitText(value, W - M * 2);
|
|
lines.forEach(l => { doc.text(l, M, y); y += 4; });
|
|
y += 1;
|
|
};
|
|
|
|
const section = (title: string) => {
|
|
if (y > H - 30) { doc.addPage(); y = M; }
|
|
doc.setFillColor(...accent as [number, number, number]);
|
|
doc.rect(M, y, W - M * 2, 7, 'F');
|
|
doc.setTextColor(255); doc.setFontSize(8); doc.setFont('helvetica', 'bold');
|
|
doc.text(title, M + 3, y + 4.5);
|
|
doc.setTextColor(30);
|
|
y += 10;
|
|
};
|
|
|
|
// ── Paciente ──
|
|
section('IDENTIFICAÇÃO DO CASO');
|
|
field('Paciente', t.pacienteNome);
|
|
field('Responsável', t.responsavel);
|
|
field('Aparelho', t.tipoAparelho);
|
|
field('Data de Início', new Date(t.dataInicio).toLocaleDateString('pt-BR'));
|
|
field('Status', t.status);
|
|
field('Fase Atual', t.faseAtual);
|
|
field('Fio Atual', t.fioAtual);
|
|
field('Progresso', t.percentualConcluido != null ? `${t.percentualConcluido}%` : undefined);
|
|
|
|
// ── Triagem ──
|
|
if (t.triagem) {
|
|
section('TRIAGEM CLÍNICA');
|
|
field('Queixa Principal', t.triagem.queixaPrincipal);
|
|
field('Histórico Médico', t.triagem.historicoMedico);
|
|
field('Medicação', t.triagem.medicacao);
|
|
field('Alergias', t.triagem.alergias);
|
|
field('Hábitos', t.triagem.habitos);
|
|
if (t.triagem.respiracaoBucal != null)
|
|
field('Respiração Bucal', t.triagem.respiracaoBucal ? 'SIM' : 'NÃO');
|
|
}
|
|
|
|
// ── Diagnóstico ──
|
|
if (t.diagnostico) {
|
|
section('DIAGNÓSTICO ORTODÔNTICO');
|
|
field('Classe de Angle', t.diagnostico.classeAngle);
|
|
field('Perfil', t.diagnostico.perfil);
|
|
field('Apinhamento', t.diagnostico.apinhamento);
|
|
field('Mordida', t.diagnostico.mordida);
|
|
field('Linha Média', t.diagnostico.linhaMedia);
|
|
}
|
|
|
|
// ── Plano de tratamento ──
|
|
section('PLANO DE TRATAMENTO');
|
|
field('Extrações', t.extracoes);
|
|
field('Elásticos', t.elasticos);
|
|
field('IPR Planejado', t.iprPlanejado);
|
|
field('Tempo Estimado', t.tempoEstimado);
|
|
field('Sequência de Fios', t.sequenciaFios);
|
|
|
|
// ── Evoluções (últimas 5) ──
|
|
section('EVOLUÇÕES RECENTES');
|
|
const ultimas = [...t.evolucoes].sort((a, b) => new Date(b.data).getTime() - new Date(a.data).getTime()).slice(0, 5);
|
|
for (const ev of ultimas) {
|
|
if (y > H - 30) { doc.addPage(); y = M; }
|
|
doc.setFontSize(7.5); doc.setFont('helvetica', 'bold'); doc.setTextColor(30);
|
|
doc.text(`${new Date(ev.data).toLocaleDateString('pt-BR')} — ${ev.tipo}`, M, y); y += 4;
|
|
doc.setFontSize(7); doc.setFont('helvetica', 'normal'); doc.setTextColor(60);
|
|
doc.text(ev.procedimento, M + 3, y); y += 4;
|
|
if (ev.fio || ev.cooperacao || ev.higiene != null) {
|
|
const tags = [ev.fio && `Fio: ${ev.fio}`, ev.cooperacao && `Coop: ${ev.cooperacao}`, ev.higiene != null && `Higiene: ${ev.higiene}/10`].filter(Boolean).join(' | ');
|
|
doc.setTextColor(120); doc.setFontSize(6.5);
|
|
doc.text(tags, M + 3, y); y += 5;
|
|
}
|
|
y += 1;
|
|
}
|
|
|
|
// ── Fotos ──
|
|
const sessoes = await HybridBackend.getOrtoSessoes(t.id);
|
|
const fotosIniciais = sessoes.sessoes?.find((s: any) => s.tipo === 'inicial')?.fotos || sessoes.sessoes?.[0]?.fotos || [];
|
|
if (fotosIniciais.length > 0) {
|
|
section('DOCUMENTAÇÃO FOTOGRÁFICA');
|
|
const COLS = 4;
|
|
const cellW = (W - M * 2 - (COLS - 1) * 3) / COLS;
|
|
const cellH = cellW * 0.75;
|
|
let col = 0, rowY = y;
|
|
for (const f of fotosIniciais.slice(0, 12)) {
|
|
if (rowY + cellH > H - 20) { doc.addPage(); rowY = M; col = 0; }
|
|
const x = M + col * (cellW + 3);
|
|
const dataUrl = await imgToDataUrl(f.url);
|
|
if (dataUrl) doc.addImage(dataUrl, 'JPEG', x, rowY, cellW, cellH);
|
|
col++;
|
|
if (col >= COLS) { col = 0; rowY += cellH + 3; }
|
|
}
|
|
y = rowY + (col > 0 ? cellH + 3 : 0) + 5;
|
|
}
|
|
|
|
doc.setTextColor(150); doc.setFontSize(6);
|
|
doc.text(`SCOREODONTO.COM · CASO CLÍNICO CONFIDENCIAL · ${t.pacienteNome}`, W / 2, H - 6, { align: 'center' });
|
|
doc.save(`CASO_${t.pacienteNome.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`);
|
|
}
|
|
|
|
// ─── Componente de botões ─────────────────────────────────────────
|
|
|
|
interface Props { t: TratamentoOrto; }
|
|
|
|
export const OrthoReports: React.FC<Props> = ({ t }) => {
|
|
const toast = useToast();
|
|
const [loading, setLoading] = useState<string | null>(null);
|
|
|
|
const run = async (tipo: string, fn: () => Promise<void>) => {
|
|
setLoading(tipo);
|
|
try { await fn(); toast.success('PDF GERADO E BAIXADO!'); }
|
|
catch (err) { console.error(err); toast.error('ERRO AO GERAR PDF.'); }
|
|
finally { setLoading(null); }
|
|
};
|
|
|
|
const btns = [
|
|
{
|
|
id: 'foto', icon: <Camera size={15} />, label: 'DOCUMENTAÇÃO FOTOGRÁFICA',
|
|
desc: 'Grade completa dos 15 slots com fotos',
|
|
color: 'bg-emerald-600 hover:bg-emerald-700',
|
|
fn: () => gerarPDFDocumentacao(t),
|
|
},
|
|
{
|
|
id: 'evolucao', icon: <Activity size={15} />, label: 'EVOLUÇÃO ORTODÔNTICA',
|
|
desc: 'Timeline cronológico de todos os atendimentos',
|
|
color: 'bg-emerald-600 hover:bg-emerald-700',
|
|
fn: () => gerarPDFEvolucao(t),
|
|
},
|
|
{
|
|
id: 'caso', icon: <GraduationCap size={15} />, label: 'CASO CLÍNICO (TUTORIA)',
|
|
desc: 'Triagem + diagnóstico + plano + evoluções + fotos',
|
|
color: 'bg-violet-600 hover:bg-violet-700',
|
|
fn: () => gerarPDFCasoClinico(t),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-gray-100 bg-gray-50 flex items-center gap-2">
|
|
<FileDown size={16} className="text-teal-500" />
|
|
<h3 className="font-black text-gray-700 uppercase text-sm">RELATÓRIOS E PDFs</h3>
|
|
</div>
|
|
<div className="p-5 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
{btns.map(b => (
|
|
<button key={b.id} onClick={() => run(b.id, b.fn)} disabled={!!loading}
|
|
className={`flex flex-col items-center gap-2 p-4 rounded-xl text-white transition-all disabled:opacity-50 ${b.color} shadow-sm`}>
|
|
{loading === b.id
|
|
? <Loader2 size={20} className="animate-spin" />
|
|
: b.icon}
|
|
<span className="text-[10px] font-black uppercase leading-tight text-center">{b.label}</span>
|
|
<span className="text-[9px] font-medium opacity-70 text-center leading-tight">{b.desc}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="px-5 pb-4 text-[10px] text-gray-400 font-bold uppercase text-center">
|
|
PDFs GERADOS LOCALMENTE — NENHUM DADO É ENVIADO PARA SERVIDORES EXTERNOS
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|