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 { 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 = {}; 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 = ({ t }) => { const toast = useToast(); const [loading, setLoading] = useState(null); const run = async (tipo: string, fn: () => Promise) => { 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: , 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: , 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: , 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 (

RELATÓRIOS E PDFs

{btns.map(b => ( ))}

PDFs GERADOS LOCALMENTE — NENHUM DADO É ENVIADO PARA SERVIDORES EXTERNOS

); };