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>
1194 lines
65 KiB
TypeScript
1194 lines
65 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Plus, Activity, ArrowRight, ArrowLeft, X, AlertTriangle, Info, ShieldAlert,
|
||
Camera, Upload, CheckCircle, Clock, Stethoscope, FileText, Heart, Sparkles,
|
||
ChevronDown, ChevronUp, User, Calendar, Percent, Scissors, RotateCcw,
|
||
Wrench, ShieldCheck, Droplets, FileDown, ClipboardCheck, AlertCircle,
|
||
Brush, Crosshair
|
||
} from 'lucide-react';
|
||
import { HybridBackend } from '../services/backend.ts';
|
||
import { TratamentoOrto, EvolucaoOrto, TipoEventoOrto, CooperacaoOrto, Paciente } from '../types.ts';
|
||
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
|
||
import { useToast } from '../contexts/ToastContext.tsx';
|
||
import { PageHeader } from '../components/PageHeader.tsx';
|
||
import { OrthoPhotoWorkspace } from './OrthoPhotoWorkspace.tsx';
|
||
import { TutoriaSection } from './TutoriaSection.tsx';
|
||
|
||
/* ================================================================
|
||
HELPER: calcula idade a partir de data de nascimento
|
||
================================================================ */
|
||
function calcAge(dob?: string): number | null {
|
||
if (!dob) return null;
|
||
const birth = new Date(dob);
|
||
const today = new Date();
|
||
let age = today.getFullYear() - birth.getFullYear();
|
||
const m = today.getMonth() - birth.getMonth();
|
||
if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
|
||
return age;
|
||
}
|
||
|
||
function tipoPacienteLabel(age: number | null): string {
|
||
if (age === null) return '';
|
||
if (age < 10) return 'INFANTIL';
|
||
if (age <= 17) return 'ADOLESCENTE';
|
||
return 'ADULTO';
|
||
}
|
||
|
||
const COOPERATION_COLORS: Record<string, string> = {
|
||
'Boa': 'bg-green-100 text-green-700',
|
||
'Média': 'bg-amber-100 text-amber-700',
|
||
'Ruim': 'bg-red-100 text-red-700',
|
||
};
|
||
|
||
const FLAG_STYLES: Record<string, { bg: string; text: string; icon: React.ReactNode }> = {
|
||
'danger': { bg: 'bg-red-50 border-red-200', text: 'text-red-700', icon: <ShieldAlert size={14} /> },
|
||
'warning': { bg: 'bg-amber-50 border-amber-200', text: 'text-amber-700', icon: <AlertTriangle size={14} /> },
|
||
'info': { bg: 'bg-blue-50 border-blue-200', text: 'text-blue-600', icon: <Info size={14} /> },
|
||
};
|
||
|
||
const PROCEDURE_BUTTONS = [
|
||
{ label: 'TROCA FIO', icon: <RotateCcw size={14} /> },
|
||
{ label: 'ELÁSTICO', icon: <Sparkles size={14} /> },
|
||
{ label: 'IPR', icon: <Scissors size={14} /> },
|
||
{ label: 'RECOLAGEM', icon: <Wrench size={14} /> },
|
||
{ label: 'AJUSTE TORQUE', icon: <RotateCcw size={14} /> },
|
||
{ label: 'PROFILAXIA', icon: <Droplets size={14} /> },
|
||
];
|
||
|
||
/* ================================================================
|
||
SECTION 1 – PATIENT HEADER (Dashboard fixo)
|
||
================================================================ */
|
||
const PatientHeader: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const age = calcAge(t.dataNascimento);
|
||
const tipoLabel = tipoPacienteLabel(age);
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl p-5 shadow-sm">
|
||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||
<div className="flex items-center gap-4">
|
||
<div className="w-14 h-14 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center font-bold text-lg flex-shrink-0">
|
||
{t.pacienteNome.substring(0, 2)}
|
||
</div>
|
||
<div>
|
||
<h2 className="text-xl font-bold text-gray-900 uppercase">{t.pacienteNome} {age !== null && <span className="text-gray-500 font-normal">— {age} ANOS</span>}</h2>
|
||
<div className="flex flex-wrap gap-2 mt-1">
|
||
{tipoLabel && <span className="text-[10px] font-bold bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full uppercase">{tipoLabel}</span>}
|
||
<span className="text-[10px] font-bold bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full uppercase">{t.classificacao || t.diagnostico?.classeAngle}</span>
|
||
<span className="text-[10px] font-bold bg-teal-100 text-teal-600 px-2 py-0.5 rounded-full uppercase">FASE: {t.faseAtual}</span>
|
||
<span className="text-[10px] font-bold bg-gray-100 text-gray-600 px-2 py-0.5 rounded-full uppercase">FIO {t.fioAtual}</span>
|
||
{t.cooperacao && <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase ${COOPERATION_COLORS[t.cooperacao] || ''}`}>COOPERAÇÃO {t.cooperacao.toUpperCase()}</span>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap gap-4 text-xs uppercase font-bold text-gray-500">
|
||
<div className="flex items-center gap-1"><Calendar size={14} /> ÚLTIMA: {t.ultimaConsulta ? new Date(t.ultimaConsulta).toLocaleDateString() : '—'}</div>
|
||
<div className="flex items-center gap-1"><Calendar size={14} className="text-teal-500" /> PRÓX: {t.proximaConsulta ? new Date(t.proximaConsulta).toLocaleDateString() : '—'}</div>
|
||
<div className="flex items-center gap-1"><Clock size={14} /> {t.mesesRestantes ?? '?'} MESES RESTANTES</div>
|
||
</div>
|
||
</div>
|
||
{/* Progress bar */}
|
||
{t.percentualConcluido != null && (
|
||
<div className="mt-4">
|
||
<div className="flex justify-between text-[10px] font-bold text-gray-500 uppercase mb-1">
|
||
<span>PROGRESSO DO TRATAMENTO</span>
|
||
<span>{t.percentualConcluido}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-100 rounded-full h-2.5">
|
||
<div className="bg-teal-500 h-2.5 rounded-full transition-all duration-500" style={{ width: `${t.percentualConcluido}%` }}></div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{/* Flags */}
|
||
{t.flags && t.flags.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 mt-3">
|
||
{t.flags.map((f, i) => {
|
||
const s = FLAG_STYLES[f.tipo] || FLAG_STYLES.info;
|
||
return (
|
||
<div key={i} className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-bold uppercase ${s.bg} ${s.text}`}>
|
||
{s.icon} {f.mensagem}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
{t.responsavel && <p className="mt-2 text-[10px] text-gray-400 font-bold uppercase">RESPONSÁVEL: {t.responsavel}</p>}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 2 – TRIAGEM CLÍNICA
|
||
================================================================ */
|
||
const ClinicalTriage: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const [open, setOpen] = useState(false);
|
||
const tr = t.triagem;
|
||
if (!tr) return null;
|
||
const age = calcAge(t.dataNascimento);
|
||
const isAdult = age !== null && age >= 18;
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||
<button onClick={() => setOpen(!open)} className="w-full px-5 py-3 flex justify-between items-center bg-gray-50 hover:bg-gray-100 transition-colors">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2"><Stethoscope size={16} className="text-purple-500" /> TRIAGEM CLÍNICA</h3>
|
||
{open ? <ChevronUp size={18} className="text-gray-400" /> : <ChevronDown size={18} className="text-gray-400" />}
|
||
</button>
|
||
{open && (
|
||
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-4 text-sm animate-in fade-in">
|
||
<Field label="QUEIXA PRINCIPAL" value={tr.queixaPrincipal} />
|
||
<Field label="HISTÓRICO MÉDICO" value={tr.historicoMedico} />
|
||
<Field label="MEDICAÇÃO" value={tr.medicacao} />
|
||
<Field label="ALERGIAS" value={tr.alergias} />
|
||
<Field label="RESPIRAÇÃO BUCAL" value={tr.respiracaoBucal ? 'SIM' : 'NÃO'} />
|
||
<Field label="HÁBITOS" value={tr.habitos} />
|
||
{isAdult && (
|
||
<>
|
||
<div className="col-span-full border-t pt-3 mt-1">
|
||
<span className="text-[10px] font-bold text-purple-500 uppercase">CAMPOS ADULTO</span>
|
||
</div>
|
||
<Field label="PERIODONTO" value={tr.periodonto} />
|
||
<Field label="IMPLANTES" value={tr.implantes} />
|
||
<Field label="RECESSÕES" value={tr.recessoes} />
|
||
<Field label="MOBILIDADE" value={tr.mobilidade} />
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 3 – DIAGNÓSTICO
|
||
================================================================ */
|
||
const Diagnosis: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const [open, setOpen] = useState(false);
|
||
const d = t.diagnostico;
|
||
if (!d) return null;
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||
<button onClick={() => setOpen(!open)} className="w-full px-5 py-3 flex justify-between items-center bg-gray-50 hover:bg-gray-100 transition-colors">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2"><FileText size={16} className="text-teal-500" /> DIAGNÓSTICO ORTODÔNTICO</h3>
|
||
{open ? <ChevronUp size={18} className="text-gray-400" /> : <ChevronDown size={18} className="text-gray-400" />}
|
||
</button>
|
||
{open && (
|
||
<div className="p-5 grid grid-cols-2 md:grid-cols-3 gap-4 text-sm animate-in fade-in">
|
||
<Field label="CLASSE ANGLE" value={d.classeAngle} highlight />
|
||
<Field label="PERFIL" value={d.perfil} />
|
||
<Field label="APINHAMENTO" value={d.apinhamento} highlight={d.apinhamento === 'Severo'} />
|
||
<Field label="MORDIDA" value={d.mordida} />
|
||
<Field label="LINHA MÉDIA" value={d.linhaMedia} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 4 – PLANO DE TRATAMENTO
|
||
================================================================ */
|
||
const TreatmentPlan: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const [open, setOpen] = useState(true);
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
||
<button onClick={() => setOpen(!open)} className="w-full px-5 py-3 flex justify-between items-center bg-gray-50 hover:bg-gray-100 transition-colors">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2"><Heart size={16} className="text-pink-500" /> PLANO DE TRATAMENTO</h3>
|
||
{open ? <ChevronUp size={18} className="text-gray-400" /> : <ChevronDown size={18} className="text-gray-400" />}
|
||
</button>
|
||
{open && (
|
||
<div className="p-5 space-y-3 text-sm animate-in fade-in">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
<Field label="APARELHO" value={t.tipoAparelho} highlight />
|
||
<Field label="EXTRAÇÕES" value={t.extracoes} />
|
||
<Field label="ELÁSTICOS" value={t.elasticos} />
|
||
<Field label="IPR PLANEJADO" value={t.iprPlanejado} />
|
||
<Field label="TEMPO ESTIMADO" value={t.tempoEstimado} />
|
||
{t.valor != null && <Field label="VALOR" value={`R$ ${t.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`} />}
|
||
{t.formaPagamento && <Field label="PAGAMENTO" value={t.formaPagamento} />}
|
||
</div>
|
||
{t.sequenciaFios && (
|
||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 mt-2">
|
||
<span className="text-[10px] font-bold text-gray-400 uppercase">SEQUÊNCIA DE FIOS</span>
|
||
<p className="font-bold text-gray-800 mt-1 tracking-wide">{t.sequenciaFios}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 5 – TIMELINE INTELIGENTE
|
||
================================================================ */
|
||
const Timeline: React.FC<{ evolucoes: EvolucaoOrto[] }> = ({ evolucoes }) => {
|
||
const sorted = [...evolucoes].sort((a, b) => new Date(b.data).getTime() - new Date(a.data).getTime());
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<Activity size={16} className="text-teal-500" /> LINHA DO TEMPO — {evolucoes.length} EVENTO{evolucoes.length !== 1 && 'S'}
|
||
</h3>
|
||
<div className="space-y-4 relative before:absolute before:left-[7px] before:top-2 before:bottom-2 before:w-0.5 before:bg-gray-200">
|
||
{sorted.map((ev) => (
|
||
<div key={ev.id} className="relative pl-7">
|
||
<div className={`absolute left-0 top-1.5 w-3.5 h-3.5 rounded-full z-10 border-2 ${ev.tipo === 'Falta' ? 'bg-red-500 border-red-300' : ev.tipo === 'Colagem' ? 'bg-green-500 border-green-300' : 'bg-white border-teal-500'}`}></div>
|
||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
||
<div className="flex flex-wrap justify-between items-start gap-2 mb-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-bold text-gray-900 text-sm">{new Date(ev.data).toLocaleDateString()}</span>
|
||
<span className="text-[9px] font-bold bg-gray-200 text-gray-600 px-2 py-0.5 rounded-full uppercase">{ev.tipo}</span>
|
||
</div>
|
||
<span className="text-[10px] text-teal-600 flex items-center gap-1 font-bold uppercase">
|
||
PRÓX: {new Date(ev.proximoRetorno).toLocaleDateString()} <ArrowRight size={10} />
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-gray-700 uppercase font-medium">{ev.procedimento}</p>
|
||
<div className="flex flex-wrap gap-2 mt-2">
|
||
{ev.fio && <span className="text-[9px] font-bold bg-teal-50 text-teal-600 px-2 py-0.5 rounded-full uppercase">FIO: {ev.fio}</span>}
|
||
{ev.elastico && <span className="text-[9px] font-bold bg-purple-50 text-purple-600 px-2 py-0.5 rounded-full uppercase">ELÁSTICO: {ev.elastico}</span>}
|
||
{ev.procedimentos?.map((p, i) => <span key={i} className="text-[9px] font-bold bg-green-50 text-green-600 px-2 py-0.5 rounded-full uppercase">{p}</span>)}
|
||
{ev.higiene != null && (
|
||
<span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase ${ev.higiene >= 7 ? 'bg-green-50 text-green-600' : ev.higiene >= 5 ? 'bg-amber-50 text-amber-600' : 'bg-red-50 text-red-600'}`}>
|
||
HIGIENE: {ev.higiene}/10
|
||
</span>
|
||
)}
|
||
{ev.cooperacao && <span className={`text-[9px] font-bold px-2 py-0.5 rounded-full uppercase ${COOPERATION_COLORS[ev.cooperacao] || ''}`}>COOP: {ev.cooperacao.toUpperCase()}</span>}
|
||
</div>
|
||
{ev.observacoes && <p className="text-[10px] text-gray-500 mt-2 italic uppercase">{ev.observacoes}</p>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 6 – NOVA EVOLUÇÃO
|
||
================================================================ */
|
||
const NewEvolution: React.FC<{ tratamentoId: string; currentFio?: string; onSaved: () => void }> = ({ tratamentoId, currentFio, onSaved }) => {
|
||
const toast = useToast();
|
||
const [tipo, setTipo] = useState<TipoEventoOrto>('Manutenção');
|
||
const [fio, setFio] = useState(currentFio || '');
|
||
const [elastico, setElastico] = useState('');
|
||
const [selectedProcs, setSelectedProcs] = useState<string[]>([]);
|
||
const [higiene, setHigiene] = useState(7);
|
||
const [cooperacao, setCooperacao] = useState<CooperacaoOrto>('Boa');
|
||
const [observacoes, setObservacoes] = useState('');
|
||
const [proximoRetorno, setProximoRetorno] = useState('');
|
||
|
||
const toggleProc = (label: string) => {
|
||
setSelectedProcs(prev => prev.includes(label) ? prev.filter(p => p !== label) : [...prev, label]);
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
const procSummary = [tipo, fio && `FIO ${fio}`, elastico && `ELÁSTICO ${elastico}`, ...selectedProcs].filter(Boolean).join(' + ');
|
||
try {
|
||
await HybridBackend.saveEvolucaoOrto(tratamentoId, {
|
||
tipo,
|
||
procedimento: procSummary.toUpperCase(),
|
||
proximoRetorno,
|
||
fio: fio || undefined,
|
||
elastico: elastico || undefined,
|
||
procedimentos: selectedProcs.length > 0 ? selectedProcs : undefined,
|
||
higiene,
|
||
cooperacao,
|
||
observacoes: observacoes.toUpperCase() || undefined,
|
||
});
|
||
toast.success('EVOLUÇÃO SALVA COM SUCESSO!');
|
||
// Reset
|
||
setTipo('Manutenção');
|
||
setElastico('');
|
||
setSelectedProcs([]);
|
||
setHigiene(7);
|
||
setCooperacao('Boa');
|
||
setObservacoes('');
|
||
setProximoRetorno('');
|
||
onSaved();
|
||
} catch {
|
||
toast.error('ERRO AO SALVAR EVOLUÇÃO.');
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<Plus size={16} className="text-green-500" /> NOVA EVOLUÇÃO
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">TIPO EVENTO</label>
|
||
<select value={tipo} onChange={e => setTipo(e.target.value as TipoEventoOrto)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1">
|
||
<option>Manutenção</option>
|
||
<option>Troca de fio</option>
|
||
<option>Colagem</option>
|
||
<option>Emergência</option>
|
||
<option>Falta</option>
|
||
<option>Finalização</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">FIO</label>
|
||
<select value={fio} onChange={e => setFio(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1">
|
||
<option value="">— MANTER ATUAL —</option>
|
||
<option>0.12</option>
|
||
<option>0.14</option>
|
||
<option>0.16</option>
|
||
<option>0.18</option>
|
||
<option>0.20x0.25</option>
|
||
<option>0.19x0.25</option>
|
||
<option>0.21x0.25</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">ELÁSTICO</label>
|
||
<select value={elastico} onChange={e => setElastico(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1">
|
||
<option value="">— NENHUM —</option>
|
||
<option>CLASSE II</option>
|
||
<option>CLASSE III</option>
|
||
<option>VERTICAL</option>
|
||
<option>CRUZADO</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Procedure quick buttons */}
|
||
<div className="mt-4">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">PROCEDIMENTOS REALIZADOS</label>
|
||
<div className="flex flex-wrap gap-2 mt-2">
|
||
{PROCEDURE_BUTTONS.map(pb => (
|
||
<button
|
||
key={pb.label}
|
||
type="button"
|
||
onClick={() => toggleProc(pb.label)}
|
||
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-xs font-bold uppercase border transition-all ${selectedProcs.includes(pb.label) ? 'bg-green-600 text-white border-green-600 shadow-sm' : 'bg-white text-gray-600 border-gray-300 hover:border-green-400 hover:bg-green-50'}`}
|
||
>
|
||
{pb.icon} {pb.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4 text-sm">
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">HIGIENE (1–10)</label>
|
||
<input type="range" min={1} max={10} value={higiene} onChange={e => setHigiene(Number(e.target.value))} className="w-full mt-2" />
|
||
<div className="text-center font-bold text-lg mt-1">{higiene}<span className="text-gray-400 text-xs">/10</span></div>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">COOPERAÇÃO</label>
|
||
<select value={cooperacao} onChange={e => setCooperacao(e.target.value as CooperacaoOrto)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1">
|
||
<option>Boa</option>
|
||
<option>Média</option>
|
||
<option>Ruim</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">PRÓXIMO RETORNO</label>
|
||
<input type="date" value={proximoRetorno} onChange={e => setProximoRetorno(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1" />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">OBSERVAÇÕES CLÍNICAS</label>
|
||
<textarea value={observacoes} onChange={e => setObservacoes(e.target.value.toUpperCase())} placeholder="ANOTAÇÕES LIVRES..." className="w-full border border-gray-300 rounded-lg p-3 mt-1 h-20 uppercase-textarea text-sm" />
|
||
</div>
|
||
|
||
<button onClick={handleSave} className="mt-4 w-full bg-green-600 hover:bg-green-700 text-white py-3 rounded-lg font-bold uppercase text-sm transition-colors shadow-sm flex items-center justify-center gap-2">
|
||
<CheckCircle size={16} /> SALVAR EVOLUÇÃO
|
||
</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 7 – COMPARATIVO FOTOGRÁFICO
|
||
================================================================ */
|
||
const PhotoCompare: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<Camera size={16} className="text-emerald-500" /> COMPARATIVO FOTOGRÁFICO
|
||
</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="border-2 border-dashed border-gray-200 rounded-xl p-8 flex flex-col items-center justify-center text-center bg-gray-50 min-h-[160px]">
|
||
<Camera size={32} className="text-gray-300 mb-2" />
|
||
<span className="text-xs font-bold text-gray-400 uppercase">INÍCIO</span>
|
||
{t.fotos?.find(f => f.tipo === 'inicio')?.data && (
|
||
<span className="text-[10px] text-gray-400 mt-1">{new Date(t.fotos.find(f => f.tipo === 'inicio')!.data).toLocaleDateString()}</span>
|
||
)}
|
||
</div>
|
||
<div className="border-2 border-dashed border-gray-200 rounded-xl p-8 flex flex-col items-center justify-center text-center bg-gray-50 min-h-[160px]">
|
||
<Camera size={32} className="text-gray-300 mb-2" />
|
||
<span className="text-xs font-bold text-gray-400 uppercase">ATUAL</span>
|
||
{t.fotos?.find(f => f.tipo === 'atual')?.data && (
|
||
<span className="text-[10px] text-gray-400 mt-1">{new Date(t.fotos.find(f => f.tipo === 'atual')!.data).toLocaleDateString()}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<button className="mt-4 w-full border-2 border-dashed border-gray-300 hover:border-teal-400 hover:bg-teal-50 py-3 rounded-lg text-xs font-bold text-gray-500 hover:text-teal-600 uppercase transition-colors flex items-center justify-center gap-2">
|
||
<Upload size={16} /> UPLOAD FOTOS
|
||
</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 8 – FINALIZAÇÃO
|
||
================================================================ */
|
||
const Finalization: React.FC<{ t: TratamentoOrto; onFinalize: () => void }> = ({ t, onFinalize }) => {
|
||
const toast = useToast();
|
||
const [checks, setChecks] = useState({ alinhamento: false, oclusao: false, raizes: false, estetica: false });
|
||
const [contencaoTipo, setContencaoTipo] = useState('CONTENÇÃO FIXA INFERIOR');
|
||
const [superior, setSuperior] = useState(false);
|
||
const [inferior, setInferior] = useState(true);
|
||
const [tempoUso, setTempoUso] = useState('PERMANENTE');
|
||
const allChecked = Object.values(checks).every(Boolean);
|
||
|
||
const handleFinalize = async () => {
|
||
if (!allChecked) {
|
||
toast.error('PREENCHA TODOS OS CRITÉRIOS DO CHECKLIST.');
|
||
return;
|
||
}
|
||
try {
|
||
await HybridBackend.finalizarTratamentoOrto(t.id, { tipo: contencaoTipo, superior, inferior, tempoUso });
|
||
toast.success('TRATAMENTO FINALIZADO COM SUCESSO!');
|
||
onFinalize();
|
||
} catch {
|
||
toast.error('ERRO AO FINALIZAR TRATAMENTO.');
|
||
}
|
||
};
|
||
|
||
if (t.status === 'Finalizado') {
|
||
return (
|
||
<div className="bg-green-50 border border-green-200 rounded-xl p-5 text-center">
|
||
<CheckCircle size={32} className="text-green-500 mx-auto mb-2" />
|
||
<h3 className="font-bold text-green-700 uppercase">TRATAMENTO FINALIZADO</h3>
|
||
{t.contencaoTipo && <p className="text-xs text-green-600 mt-1 uppercase">CONTENÇÃO: {t.contencaoTipo}</p>}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<ShieldCheck size={16} className="text-green-500" /> FINALIZAÇÃO E CONTENÇÃO
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{/* Checklist */}
|
||
<div>
|
||
<span className="text-[10px] font-bold text-gray-400 uppercase">CHECKLIST OBRIGATÓRIO</span>
|
||
<div className="space-y-2 mt-2">
|
||
{Object.entries({ alinhamento: 'ALINHAMENTO OK', oclusao: 'OCLUSÃO OK', raizes: 'RAÍZES PARALELAS', estetica: 'ESTÉTICA ACEITA' }).map(([key, label]) => (
|
||
<label key={key} className="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" checked={checks[key as keyof typeof checks]} onChange={() => setChecks(prev => ({ ...prev, [key]: !prev[key as keyof typeof checks] }))} className="w-4 h-4 rounded border-gray-300 text-green-600" />
|
||
<span className="font-bold uppercase text-gray-700">{label}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Contenção */}
|
||
<div className="space-y-3">
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">TIPO CONTENÇÃO</label>
|
||
<select value={contencaoTipo} onChange={e => setContencaoTipo(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1 text-sm">
|
||
<option>CONTENÇÃO FIXA INFERIOR</option>
|
||
<option>CONTENÇÃO FIXA SUPERIOR E INFERIOR</option>
|
||
<option>PLACA DE CONTENÇÃO REMOVÍVEL</option>
|
||
<option>ESSIX (ALINHADOR)</option>
|
||
</select>
|
||
</div>
|
||
<div className="flex gap-4">
|
||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" checked={superior} onChange={() => setSuperior(!superior)} className="w-4 h-4 rounded" />
|
||
<span className="font-bold uppercase text-gray-700">SUPERIOR</span>
|
||
</label>
|
||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||
<input type="checkbox" checked={inferior} onChange={() => setInferior(!inferior)} className="w-4 h-4 rounded" />
|
||
<span className="font-bold uppercase text-gray-700">INFERIOR</span>
|
||
</label>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">TEMPO DE USO</label>
|
||
<select value={tempoUso} onChange={e => setTempoUso(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white uppercase-select mt-1 text-sm">
|
||
<option>PERMANENTE</option>
|
||
<option>6 MESES</option>
|
||
<option>12 MESES</option>
|
||
<option>24 MESES</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button onClick={handleFinalize} disabled={!allChecked} className={`mt-5 w-full py-3 rounded-lg font-bold uppercase text-sm transition-colors flex items-center justify-center gap-2 ${allChecked ? 'bg-green-600 hover:bg-green-700 text-white shadow-sm' : 'bg-gray-200 text-gray-400 cursor-not-allowed'}`}>
|
||
<CheckCircle size={16} /> FINALIZAR TRATAMENTO
|
||
</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 9 – CONTENÇÃO — TERMO DE CIÊNCIA + VALOR
|
||
================================================================ */
|
||
const ContencaoTermo: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const toast = useToast();
|
||
const [valorContencao, setValorContencao] = useState(350);
|
||
const [aceite, setAceite] = useState(false);
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<ClipboardCheck size={16} className="text-teal-500" /> CONTENÇÃO — TERMO E VALOR
|
||
</h3>
|
||
|
||
{/* Termo de ciência */}
|
||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
|
||
<div className="flex items-start gap-2">
|
||
<AlertCircle size={18} className="text-amber-600 flex-shrink-0 mt-0.5" />
|
||
<div>
|
||
<p className="text-xs font-bold text-amber-700 uppercase mb-1">TERMO DE CIÊNCIA</p>
|
||
<p className="text-xs text-amber-800 leading-relaxed">
|
||
O PACIENTE DECLARA ESTAR CIENTE DE QUE A <strong>CONTENÇÃO ORTODÔNTICA</strong> (FIXA E/OU REMOVÍVEL)
|
||
É UM PROCEDIMENTO <strong>SEPARADO DO TRATAMENTO ORTODÔNTICO</strong> E <strong>NÃO ESTÁ INCLUÍDA NO VALOR
|
||
DA MANUTENÇÃO MENSAL</strong>. O CUSTO DA CONTENÇÃO SERÁ COBRADO À PARTE, CONFORME VALORES
|
||
ABAIXO INFORMADOS.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">VALOR DA CONTENÇÃO (R$)</label>
|
||
<input
|
||
type="number"
|
||
value={valorContencao}
|
||
onChange={e => setValorContencao(Number(e.target.value))}
|
||
className="w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1 text-sm font-bold"
|
||
min={0}
|
||
step={50}
|
||
/>
|
||
<p className="text-[10px] text-gray-400 mt-1">VALOR COBRADO SEPARADAMENTE DA MENSALIDADE</p>
|
||
</div>
|
||
<div className="flex flex-col justify-end">
|
||
<label className="flex items-start gap-2 cursor-pointer bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||
<input
|
||
type="checkbox"
|
||
checked={aceite}
|
||
onChange={() => setAceite(!aceite)}
|
||
className="w-4 h-4 rounded border-gray-300 text-teal-600 mt-0.5"
|
||
/>
|
||
<span className="text-xs font-bold uppercase text-gray-700 leading-relaxed">
|
||
PACIENTE CIENTE E DE ACORDO COM A COBRANÇA SEPARADA DA CONTENÇÃO
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{aceite && (
|
||
<div className="mt-3 bg-green-50 border border-green-200 rounded-lg p-3 flex items-center gap-2">
|
||
<CheckCircle size={16} className="text-green-500" />
|
||
<span className="text-xs font-bold text-green-700 uppercase">TERMO ACEITO — R$ {valorContencao.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 10 – SOLICITAÇÕES CLÍNICAS (LIMPEZA, CÁRIE, EXTRAÇÃO, RASPAGEM)
|
||
================================================================ */
|
||
const ClinicalRequests: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const toast = useToast();
|
||
const [requests, setRequests] = useState<{ tipo: string; dente?: string; obs?: string }[]>([]);
|
||
const [novoTipo, setNovoTipo] = useState('LIMPEZA / PROFILAXIA');
|
||
const [novoDente, setNovoDente] = useState('');
|
||
const [novoObs, setNovoObs] = useState('');
|
||
|
||
const REQUEST_TYPES = [
|
||
'LIMPEZA / PROFILAXIA',
|
||
'DENTE COM CÁRIE',
|
||
'DENTE PARA EXTRAIR',
|
||
'RASPAGEM PERIODONTAL',
|
||
];
|
||
|
||
const addRequest = () => {
|
||
setRequests(prev => [
|
||
...prev,
|
||
{ tipo: novoTipo, dente: novoDente.toUpperCase() || undefined, obs: novoObs.toUpperCase() || undefined }
|
||
]);
|
||
setNovoDente('');
|
||
setNovoObs('');
|
||
toast.success('SOLICITAÇÃO ADICIONADA!');
|
||
};
|
||
|
||
const removeRequest = (index: number) => {
|
||
setRequests(prev => prev.filter((_, i) => i !== index));
|
||
};
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<Stethoscope size={16} className="text-red-500" /> SOLICITAÇÕES CLÍNICAS
|
||
</h3>
|
||
|
||
{/* Lista de solicitações já adicionadas */}
|
||
{requests.length > 0 && (
|
||
<div className="space-y-2 mb-4">
|
||
{requests.map((req, i) => (
|
||
<div key={i} className="flex items-center justify-between bg-gray-50 border border-gray-200 rounded-lg p-3">
|
||
<div className="flex items-center gap-3">
|
||
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${req.tipo.includes('CÁRIE') ? 'bg-red-500' :
|
||
req.tipo.includes('EXTRAIR') ? 'bg-teal-500' :
|
||
req.tipo.includes('RASPAGEM') ? 'bg-purple-500' : 'bg-teal-500'
|
||
}`}></span>
|
||
<div>
|
||
<span className="text-xs font-bold text-gray-800 uppercase">{req.tipo}</span>
|
||
{req.dente && <span className="text-[10px] text-gray-500 ml-2">DENTE: {req.dente}</span>}
|
||
{req.obs && <p className="text-[10px] text-gray-400 italic">{req.obs}</p>}
|
||
</div>
|
||
</div>
|
||
<button onClick={() => removeRequest(i)} className="text-gray-400 hover:text-red-500 transition-colors">
|
||
<X size={16} />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Formulário para nova solicitação */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">TIPO</label>
|
||
<select value={novoTipo} onChange={e => setNovoTipo(e.target.value)} className="w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1 text-sm">
|
||
{REQUEST_TYPES.map(rt => <option key={rt}>{rt}</option>)}
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">DENTE (OPCIONAL)</label>
|
||
<input
|
||
type="text"
|
||
value={novoDente}
|
||
onChange={e => setNovoDente(e.target.value)}
|
||
placeholder="EX: 36, 14"
|
||
className="w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1 text-sm uppercase"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase">OBSERVAÇÃO</label>
|
||
<input
|
||
type="text"
|
||
value={novoObs}
|
||
onChange={e => setNovoObs(e.target.value)}
|
||
placeholder="DETALHES..."
|
||
className="w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1 text-sm uppercase"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<button onClick={addRequest} className="mt-3 w-full border-2 border-dashed border-gray-300 hover:border-teal-400 hover:bg-teal-50 py-2.5 rounded-lg text-xs font-bold text-gray-500 hover:text-teal-600 uppercase transition-colors flex items-center justify-center gap-2">
|
||
<Plus size={14} /> ADICIONAR SOLICITAÇÃO
|
||
</button>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
SECTION 11 – RELATÓRIO DE MANUTENÇÕES (GERAR + EXPORTAR)
|
||
================================================================ */
|
||
const MaintenanceReport: React.FC<{ t: TratamentoOrto }> = ({ t }) => {
|
||
const toast = useToast();
|
||
|
||
const generateReport = () => {
|
||
const sorted = [...t.evolucoes].sort((a, b) => new Date(a.data).getTime() - new Date(b.data).getTime());
|
||
const lines: string[] = [
|
||
'════════════════════════════════════════════════════════',
|
||
' RELATÓRIO DE MANUTENÇÕES ORTODÔNTICAS',
|
||
'════════════════════════════════════════════════════════',
|
||
'',
|
||
`PACIENTE: ${t.pacienteNome}`,
|
||
`APARELHO: ${t.tipoAparelho}`,
|
||
`INÍCIO: ${new Date(t.dataInicio).toLocaleDateString()}`,
|
||
`STATUS: ${t.status}`,
|
||
`FASE ATUAL: ${t.faseAtual || '—'}`,
|
||
`FIO ATUAL: ${t.fioAtual || '—'}`,
|
||
`PROGRESSO: ${t.percentualConcluido ?? '—'}%`,
|
||
'',
|
||
'────────────────────────────────────────────────────────',
|
||
` TOTAL DE ATENDIMENTOS: ${sorted.length}`,
|
||
'────────────────────────────────────────────────────────',
|
||
'',
|
||
];
|
||
|
||
sorted.forEach((ev, i) => {
|
||
lines.push(` #${i + 1} | ${new Date(ev.data).toLocaleDateString()} | ${ev.tipo}`);
|
||
lines.push(` ${ev.procedimento}`);
|
||
if (ev.fio) lines.push(` FIO: ${ev.fio}`);
|
||
if (ev.elastico) lines.push(` ELÁSTICO: ${ev.elastico}`);
|
||
if (ev.procedimentos?.length) lines.push(` PROC: ${ev.procedimentos.join(', ')}`);
|
||
if (ev.higiene != null) lines.push(` HIGIENE: ${ev.higiene}/10`);
|
||
if (ev.cooperacao) lines.push(` COOPERAÇÃO: ${ev.cooperacao}`);
|
||
if (ev.observacoes) lines.push(` OBS: ${ev.observacoes}`);
|
||
lines.push(` PRÓXIMO RETORNO: ${new Date(ev.proximoRetorno).toLocaleDateString()}`);
|
||
lines.push('');
|
||
});
|
||
|
||
lines.push('────────────────────────────────────────────────────────');
|
||
lines.push(` GERADO EM: ${new Date().toLocaleString()}`);
|
||
lines.push(' SCOREODONTO — GESTÃO ORTODÔNTICA');
|
||
lines.push('════════════════════════════════════════════════════════');
|
||
|
||
return lines.join('\n');
|
||
};
|
||
|
||
const handleExport = () => {
|
||
const content = generateReport();
|
||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `RELATORIO_ORTO_${t.pacienteNome.replace(/\s+/g, '_')}_${new Date().toISOString().split('T')[0]}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
toast.success('RELATÓRIO EXPORTADO COM SUCESSO!');
|
||
};
|
||
|
||
const handleCopy = () => {
|
||
const content = generateReport();
|
||
navigator.clipboard.writeText(content).then(() => {
|
||
toast.success('RELATÓRIO COPIADO PARA ÁREA DE TRANSFERÊNCIA!');
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="bg-white border border-gray-200 rounded-xl shadow-sm p-5">
|
||
<h3 className="font-bold text-gray-700 uppercase text-sm flex items-center gap-2 mb-4">
|
||
<FileDown size={16} className="text-teal-500" /> RELATÓRIO DE MANUTENÇÕES
|
||
</h3>
|
||
|
||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4 mb-4">
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-center">
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-800">{t.evolucoes.length}</p>
|
||
<p className="text-[10px] font-bold text-gray-400 uppercase">ATENDIMENTOS</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-800">{new Date(t.dataInicio).toLocaleDateString()}</p>
|
||
<p className="text-[10px] font-bold text-gray-400 uppercase">INÍCIO</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-800">{t.percentualConcluido ?? '—'}%</p>
|
||
<p className="text-[10px] font-bold text-gray-400 uppercase">PROGRESSO</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-2xl font-bold text-gray-800">{t.mesesRestantes ?? '—'}</p>
|
||
<p className="text-[10px] font-bold text-gray-400 uppercase">MESES REST.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<button onClick={handleExport} className="bg-teal-600 hover:bg-teal-700 text-white py-3 rounded-lg font-bold uppercase text-sm transition-colors shadow-sm flex items-center justify-center gap-2">
|
||
<FileDown size={16} /> EXPORTAR RELATÓRIO (.TXT)
|
||
</button>
|
||
<button onClick={handleCopy} className="bg-gray-600 hover:bg-gray-700 text-white py-3 rounded-lg font-bold uppercase text-sm transition-colors shadow-sm flex items-center justify-center gap-2">
|
||
<ClipboardCheck size={16} /> COPIAR PARA ÁREA DE TRANSFERÊNCIA
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
REUSABLE: Campo label + value
|
||
================================================================ */
|
||
const Field: React.FC<{ label: string; value?: string | null; highlight?: boolean }> = ({ label, value, highlight }) => (
|
||
<div>
|
||
<span className="text-[10px] font-bold text-gray-400 uppercase">{label}</span>
|
||
<p className={`font-bold uppercase mt-0.5 ${highlight ? 'text-teal-600' : 'text-gray-800'}`}>{value || '—'}</p>
|
||
</div>
|
||
);
|
||
|
||
/* ================================================================
|
||
DETAIL PANEL — modal fullscreen com todas as seções
|
||
================================================================ */
|
||
const OrthoDetailPanel: React.FC<{ treatment: TratamentoOrto; onClose: () => void }> = ({ treatment, onClose }) => {
|
||
const [t, setT] = useState(treatment);
|
||
const clinicaId = HybridBackend.getActiveWorkspace()?.id;
|
||
const { refresh: refreshList } = useHybridBackend<TratamentoOrto[]>(HybridBackend.getOrtoTreatments);
|
||
|
||
const handleEvolucaoSaved = useCallback(() => {
|
||
// Re-fetch to update local state
|
||
HybridBackend.getOrtoTreatments().then(all => {
|
||
const updated = all.find(tr => tr.id === t.id);
|
||
if (updated) setT(updated);
|
||
});
|
||
}, [t.id]);
|
||
|
||
useEffect(() => {
|
||
const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||
document.addEventListener('keydown', handleKeyDown);
|
||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||
}, [onClose]);
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-0">
|
||
<div className="bg-gray-50 shadow-2xl w-full h-dvh md:w-[96vw] md:h-[98vh] rounded-none md:rounded-xl flex flex-col overflow-hidden animate-in fade-in zoom-in-50 duration-200">
|
||
{/* Top bar */}
|
||
<div className="px-5 py-3 border-b border-gray-200 flex justify-between items-center flex-shrink-0 bg-white">
|
||
<button onClick={onClose} className="flex items-center gap-2 text-gray-500 hover:text-gray-800 text-xs font-bold uppercase transition-colors">
|
||
<ArrowLeft size={16} /> VOLTAR À LISTA
|
||
</button>
|
||
<div className="flex items-center gap-2">
|
||
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase ${t.status === 'Finalizado' ? 'bg-green-100 text-green-700' : t.status === 'Contenção' ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'}`}>
|
||
{t.status}
|
||
</span>
|
||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg p-1.5 transition-colors"><X size={20} /></button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scrollable content */}
|
||
<div className="flex-1 overflow-y-auto p-4 lg:p-6 space-y-5">
|
||
<PatientHeader t={t} />
|
||
<ClinicalTriage t={t} />
|
||
<Diagnosis t={t} />
|
||
<TreatmentPlan t={t} />
|
||
<Timeline evolucoes={t.evolucoes} />
|
||
<NewEvolution tratamentoId={t.id} currentFio={t.fioAtual} onSaved={handleEvolucaoSaved} />
|
||
<OrthoPhotoWorkspace tratamentoId={t.id} clinicaId={clinicaId} pacienteNome={t.pacienteNome} />
|
||
<TutoriaSection tratamentoId={t.id} clinicaId={clinicaId} pacienteNome={t.pacienteNome} />
|
||
<ContencaoTermo t={t} />
|
||
<ClinicalRequests t={t} />
|
||
<MaintenanceReport t={t} />
|
||
<Finalization t={t} onFinalize={() => { refreshList(); onClose(); }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/* ================================================================
|
||
MAIN VIEW — ORTHOVIEW (preservando listagem original)
|
||
================================================================ */
|
||
/* ================================================================
|
||
NOVO TRATAMENTO — modal de criação
|
||
================================================================ */
|
||
const STATUS_OPTS = ['Em Tratamento', 'Contenção', 'Finalizado'];
|
||
|
||
const NovoTratamentoModal: React.FC<{ onClose: () => void; onCreated: () => void }> = ({ onClose, onCreated }) => {
|
||
const toast = useToast();
|
||
const [salvando, setSalvando] = useState(false);
|
||
const [f, setF] = useState<any>({
|
||
pacienteId: '', pacienteNome: '', dataNascimento: '', responsavel: '',
|
||
dataInicio: new Date().toISOString().slice(0, 10), tipoAparelho: '', status: 'Em Tratamento',
|
||
faseAtual: '', fioAtual: '', cooperacao: 'Boa', tempoEstimado: '', valor: '', formaPagamento: '',
|
||
extracoes: '', sequenciaFios: '', elasticos: '', iprPlanejado: '',
|
||
classeAngle: '', perfil: '', apinhamento: '', mordida: '', linhaMedia: '',
|
||
queixaPrincipal: '', historicoMedico: '', medicacao: '', alergias: '', habitos: '', respiracaoBucal: false,
|
||
});
|
||
const set = (k: string, v: any) => setF((p: any) => ({ ...p, [k]: v }));
|
||
const up = (v: string) => v?.toUpperCase() || undefined;
|
||
|
||
// Busca de paciente no cadastro (vincula paciente_id; digitar livre = paciente avulso)
|
||
const [resultados, setResultados] = useState<Paciente[]>([]);
|
||
const [buscando, setBuscando] = useState(false);
|
||
const [showRes, setShowRes] = useState(false);
|
||
const [criandoPaciente, setCriandoPaciente] = useState(false);
|
||
const [pacInfo, setPacInfo] = useState<{ cpf?: string; telefone?: string } | null>(null);
|
||
useEffect(() => {
|
||
const term = f.pacienteNome.trim();
|
||
if (f.pacienteId || term.length < 2) { setResultados([]); return; }
|
||
let active = true;
|
||
setBuscando(true);
|
||
const h = setTimeout(async () => {
|
||
try {
|
||
const r = await HybridBackend.getPacientes(term);
|
||
if (active) { setResultados((r.data || []).slice(0, 8)); setShowRes(true); }
|
||
} catch { /* ignore */ }
|
||
finally { if (active) setBuscando(false); }
|
||
}, 350);
|
||
return () => { active = false; clearTimeout(h); };
|
||
}, [f.pacienteNome, f.pacienteId]);
|
||
|
||
const selectPaciente = (p: Paciente) => {
|
||
setF((prev: any) => ({ ...prev, pacienteId: p.id, pacienteNome: p.nome, dataNascimento: p.dataNascimento || prev.dataNascimento }));
|
||
setPacInfo({ cpf: p.cpf, telefone: p.telefone });
|
||
setResultados([]); setShowRes(false);
|
||
};
|
||
|
||
// Cria um paciente mínimo (só nome) na base da clínica e já vincula ao tratamento.
|
||
const criarPaciente = async () => {
|
||
const nome = f.pacienteNome.trim();
|
||
if (nome.length < 2) return;
|
||
setCriandoPaciente(true);
|
||
try {
|
||
const res = await HybridBackend.savePaciente({ nome });
|
||
if (res?.success && res.data?.id) {
|
||
setF((prev: any) => ({ ...prev, pacienteId: res.data.id, pacienteNome: nome.toUpperCase() }));
|
||
setResultados([]); setShowRes(false);
|
||
toast.success('PACIENTE CADASTRADO E VINCULADO!');
|
||
} else { toast.error('ERRO AO CADASTRAR PACIENTE.'); }
|
||
} catch { toast.error('ERRO AO CADASTRAR PACIENTE.'); }
|
||
finally { setCriandoPaciente(false); }
|
||
};
|
||
|
||
const submit = async () => {
|
||
if (!f.pacienteNome.trim()) { toast.error('NOME DO PACIENTE É OBRIGATÓRIO.'); return; }
|
||
if (!f.tipoAparelho.trim()) { toast.error('TIPO DE APARELHO É OBRIGATÓRIO.'); return; }
|
||
setSalvando(true);
|
||
try {
|
||
const payload: Partial<TratamentoOrto> = {
|
||
pacienteId: f.pacienteId || undefined,
|
||
pacienteNome: f.pacienteNome.toUpperCase(),
|
||
dataNascimento: f.dataNascimento || undefined,
|
||
responsavel: up(f.responsavel),
|
||
dataInicio: f.dataInicio,
|
||
tipoAparelho: f.tipoAparelho.toUpperCase(),
|
||
status: f.status,
|
||
faseAtual: up(f.faseAtual),
|
||
fioAtual: f.fioAtual || undefined,
|
||
cooperacao: f.cooperacao || undefined,
|
||
tempoEstimado: up(f.tempoEstimado),
|
||
valor: f.valor ? Number(f.valor) : undefined,
|
||
formaPagamento: up(f.formaPagamento),
|
||
extracoes: up(f.extracoes),
|
||
sequenciaFios: f.sequenciaFios || undefined,
|
||
elasticos: up(f.elasticos),
|
||
iprPlanejado: up(f.iprPlanejado),
|
||
diagnostico: (f.classeAngle || f.perfil || f.apinhamento || f.mordida || f.linhaMedia) ? {
|
||
classeAngle: f.classeAngle || undefined, perfil: f.perfil || undefined,
|
||
apinhamento: f.apinhamento || undefined, mordida: f.mordida || undefined, linhaMedia: up(f.linhaMedia),
|
||
} : undefined,
|
||
triagem: (f.queixaPrincipal || f.historicoMedico || f.medicacao || f.alergias || f.habitos || f.respiracaoBucal) ? {
|
||
queixaPrincipal: up(f.queixaPrincipal), historicoMedico: up(f.historicoMedico), medicacao: up(f.medicacao),
|
||
alergias: up(f.alergias), habitos: up(f.habitos), respiracaoBucal: !!f.respiracaoBucal,
|
||
} : undefined,
|
||
};
|
||
const res = await HybridBackend.createTratamentoOrto(payload);
|
||
if (!res.success) { toast.error(res.message || 'ERRO AO CRIAR TRATAMENTO.'); return; }
|
||
toast.success('TRATAMENTO CRIADO!');
|
||
onCreated();
|
||
} catch { toast.error('ERRO AO CRIAR TRATAMENTO.'); }
|
||
finally { setSalvando(false); }
|
||
};
|
||
|
||
const inp = 'w-full border border-gray-300 rounded-lg p-2.5 bg-white mt-1 text-sm';
|
||
const lbl = 'text-[10px] font-bold text-gray-400 uppercase';
|
||
|
||
return (
|
||
<div className="fixed inset-0 bg-black/50 z-[60] flex items-center justify-center p-4 backdrop-blur-sm">
|
||
<div className="bg-white rounded-xl shadow-2xl w-full max-w-3xl max-h-[92vh] flex flex-col">
|
||
<div className="px-5 py-4 border-b flex items-center justify-between flex-shrink-0">
|
||
<h3 className="font-bold text-gray-800 uppercase text-sm flex items-center gap-2"><Plus size={16} className="text-teal-600" /> NOVO TRATAMENTO ORTODÔNTICO</h3>
|
||
<button onClick={onClose} className="p-1.5 hover:bg-gray-100 rounded-lg"><X size={18} /></button>
|
||
</div>
|
||
<div className="flex-1 overflow-y-auto p-5 space-y-6">
|
||
<section>
|
||
<p className="text-[11px] font-black text-purple-500 uppercase mb-2">PACIENTE</p>
|
||
<div className="relative mb-3">
|
||
<label className={lbl}>
|
||
PACIENTE *{' '}
|
||
{f.pacienteId
|
||
? <span className="text-green-600">● VINCULADO AO CADASTRO</span>
|
||
: <span className="text-gray-300 normal-case">(busca por nome, CPF ou telefone — ou digite um nome avulso)</span>}
|
||
</label>
|
||
<input
|
||
className={inp}
|
||
value={f.pacienteNome}
|
||
onChange={e => { set('pacienteNome', e.target.value); set('pacienteId', ''); setPacInfo(null); }}
|
||
onFocus={() => { if (resultados.length) setShowRes(true); }}
|
||
placeholder="BUSCAR POR NOME, CPF OU TELEFONE"
|
||
autoComplete="off"
|
||
/>
|
||
{buscando && <span className="absolute right-3 top-9 text-[9px] text-gray-400 uppercase">buscando…</span>}
|
||
{!buscando && f.pacienteId && (
|
||
<button type="button" onClick={() => { set('pacienteId', ''); setPacInfo(null); }} className="absolute right-3 top-9 text-[9px] font-bold text-gray-400 hover:text-red-500 uppercase">desvincular</button>
|
||
)}
|
||
{f.pacienteId && pacInfo && (pacInfo.cpf || pacInfo.telefone) && (
|
||
<p className="text-[10px] text-gray-400 mt-1 ml-1">
|
||
{pacInfo.cpf && <>CPF: <strong className="text-gray-600">{pacInfo.cpf}</strong></>}
|
||
{pacInfo.cpf && pacInfo.telefone && ' · '}
|
||
{pacInfo.telefone && <>TEL: <strong className="text-gray-600">{pacInfo.telefone}</strong></>}
|
||
</p>
|
||
)}
|
||
{showRes && !f.pacienteId && f.pacienteNome.trim().length >= 2 && (
|
||
<div className="absolute z-10 left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-56 overflow-y-auto">
|
||
{resultados.map(p => (
|
||
<button key={p.id} type="button" onClick={() => selectPaciente(p)} className="w-full text-left px-3 py-2 hover:bg-teal-50 text-xs border-b border-gray-50">
|
||
<span className="font-bold text-gray-800 uppercase">{p.nome}</span>
|
||
{p.cpf && <span className="text-gray-400 ml-2">CPF {p.cpf}</span>}
|
||
{p.telefone && <span className="text-gray-400 ml-2">TEL {p.telefone}</span>}
|
||
{p.dataNascimento && <span className="text-gray-300 ml-2">{new Date(p.dataNascimento).toLocaleDateString()}</span>}
|
||
</button>
|
||
))}
|
||
{!buscando && resultados.length === 0 && (
|
||
<p className="px-3 py-2 text-[10px] text-gray-400 uppercase">Nenhum paciente encontrado na base.</p>
|
||
)}
|
||
<button type="button" onClick={criarPaciente} disabled={criandoPaciente}
|
||
className="w-full text-left px-3 py-2.5 hover:bg-emerald-50 text-xs text-emerald-700 font-black flex items-center gap-2 border-t border-gray-100 disabled:opacity-50">
|
||
<Plus size={13} /> {criandoPaciente ? 'CADASTRANDO…' : `Cadastrar "${f.pacienteNome.trim().toUpperCase()}" como novo paciente`}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div><label className={lbl}>DATA NASCIMENTO</label><input type="date" className={inp} value={f.dataNascimento} onChange={e => set('dataNascimento', e.target.value)} /></div>
|
||
<div><label className={lbl}>RESPONSÁVEL</label><input className={inp} value={f.responsavel} onChange={e => set('responsavel', e.target.value)} /></div>
|
||
</div>
|
||
</section>
|
||
<section>
|
||
<p className="text-[11px] font-black text-pink-500 uppercase mb-2">PLANO DE TRATAMENTO</p>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div><label className={lbl}>DATA INÍCIO *</label><input type="date" className={inp} value={f.dataInicio} onChange={e => set('dataInicio', e.target.value)} /></div>
|
||
<div><label className={lbl}>TIPO APARELHO *</label><input className={inp} value={f.tipoAparelho} onChange={e => set('tipoAparelho', e.target.value)} placeholder="AUTOLIGADO METÁLICO" /></div>
|
||
<div><label className={lbl}>STATUS</label><select className={inp} value={f.status} onChange={e => set('status', e.target.value)}>{STATUS_OPTS.map(s => <option key={s}>{s}</option>)}</select></div>
|
||
<div><label className={lbl}>FASE ATUAL</label><input className={inp} value={f.faseAtual} onChange={e => set('faseAtual', e.target.value)} placeholder="NIVELAMENTO" /></div>
|
||
<div><label className={lbl}>FIO ATUAL</label><input className={inp} value={f.fioAtual} onChange={e => set('fioAtual', e.target.value)} placeholder="0.16" /></div>
|
||
<div><label className={lbl}>COOPERAÇÃO</label><select className={inp} value={f.cooperacao} onChange={e => set('cooperacao', e.target.value)}><option>Boa</option><option>Média</option><option>Ruim</option></select></div>
|
||
<div><label className={lbl}>TEMPO ESTIMADO</label><input className={inp} value={f.tempoEstimado} onChange={e => set('tempoEstimado', e.target.value)} placeholder="18–24 MESES" /></div>
|
||
<div><label className={lbl}>VALOR (R$)</label><input type="number" className={inp} value={f.valor} onChange={e => set('valor', e.target.value)} /></div>
|
||
<div><label className={lbl}>FORMA PAGAMENTO</label><input className={inp} value={f.formaPagamento} onChange={e => set('formaPagamento', e.target.value)} placeholder="PARCELADO 24X" /></div>
|
||
<div><label className={lbl}>EXTRAÇÕES</label><input className={inp} value={f.extracoes} onChange={e => set('extracoes', e.target.value)} /></div>
|
||
<div className="md:col-span-2"><label className={lbl}>SEQUÊNCIA DE FIOS</label><input className={inp} value={f.sequenciaFios} onChange={e => set('sequenciaFios', e.target.value)} placeholder="0.12 → 0.14 → 0.16" /></div>
|
||
<div><label className={lbl}>ELÁSTICOS</label><input className={inp} value={f.elasticos} onChange={e => set('elasticos', e.target.value)} /></div>
|
||
<div><label className={lbl}>IPR PLANEJADO</label><input className={inp} value={f.iprPlanejado} onChange={e => set('iprPlanejado', e.target.value)} /></div>
|
||
</div>
|
||
</section>
|
||
<section>
|
||
<p className="text-[11px] font-black text-teal-500 uppercase mb-2">DIAGNÓSTICO</p>
|
||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||
<div><label className={lbl}>CLASSE ANGLE</label><select className={inp} value={f.classeAngle} onChange={e => set('classeAngle', e.target.value)}><option value="">—</option><option>I</option><option>II</option><option>III</option></select></div>
|
||
<div><label className={lbl}>PERFIL</label><select className={inp} value={f.perfil} onChange={e => set('perfil', e.target.value)}><option value="">—</option><option>Reto</option><option>Convexo</option><option>Côncavo</option></select></div>
|
||
<div><label className={lbl}>APINHAMENTO</label><select className={inp} value={f.apinhamento} onChange={e => set('apinhamento', e.target.value)}><option value="">—</option><option>Leve</option><option>Moderado</option><option>Severo</option></select></div>
|
||
<div><label className={lbl}>MORDIDA</label><select className={inp} value={f.mordida} onChange={e => set('mordida', e.target.value)}><option value="">—</option><option>Normal</option><option>Aberta</option><option>Profunda</option><option>Cruzada</option></select></div>
|
||
<div><label className={lbl}>LINHA MÉDIA</label><input className={inp} value={f.linhaMedia} onChange={e => set('linhaMedia', e.target.value)} placeholder="1MM ESQ" /></div>
|
||
</div>
|
||
</section>
|
||
<section>
|
||
<p className="text-[11px] font-black text-emerald-500 uppercase mb-2">TRIAGEM</p>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<div><label className={lbl}>QUEIXA PRINCIPAL</label><input className={inp} value={f.queixaPrincipal} onChange={e => set('queixaPrincipal', e.target.value)} /></div>
|
||
<div><label className={lbl}>HISTÓRICO MÉDICO</label><input className={inp} value={f.historicoMedico} onChange={e => set('historicoMedico', e.target.value)} /></div>
|
||
<div><label className={lbl}>MEDICAÇÃO</label><input className={inp} value={f.medicacao} onChange={e => set('medicacao', e.target.value)} /></div>
|
||
<div><label className={lbl}>ALERGIAS</label><input className={inp} value={f.alergias} onChange={e => set('alergias', e.target.value)} /></div>
|
||
<div><label className={lbl}>HÁBITOS</label><input className={inp} value={f.habitos} onChange={e => set('habitos', e.target.value)} /></div>
|
||
<label className="flex items-center gap-2 mt-6 text-sm cursor-pointer"><input type="checkbox" checked={f.respiracaoBucal} onChange={e => set('respiracaoBucal', e.target.checked)} className="w-4 h-4" /><span className="font-bold uppercase text-gray-700 text-xs">RESPIRAÇÃO BUCAL</span></label>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
<div className="px-5 py-4 border-t flex gap-3 flex-shrink-0">
|
||
<button onClick={onClose} className="flex-1 py-2.5 border border-gray-200 rounded-lg text-xs font-bold uppercase hover:bg-gray-50">CANCELAR</button>
|
||
<button onClick={submit} disabled={salvando} className="flex-1 py-2.5 bg-teal-600 hover:bg-teal-700 text-white rounded-lg text-xs font-bold uppercase disabled:opacity-40 flex items-center justify-center gap-2">{salvando ? 'SALVANDO...' : <><Plus size={14} /> CRIAR TRATAMENTO</>}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export const OrthoView: React.FC = () => {
|
||
const { data: treatments, refresh } = useHybridBackend<TratamentoOrto[]>(HybridBackend.getOrtoTreatments);
|
||
const [selectedTreatment, setSelectedTreatment] = useState<TratamentoOrto | null>(null);
|
||
const [showNovo, setShowNovo] = useState(false);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader title="GESTÃO ORTODÔNTICA">
|
||
<button onClick={() => setShowNovo(true)} className="bg-teal-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-teal-700 text-xs font-bold uppercase transition-colors shadow-sm">
|
||
<Plus size={18} /> NOVO TRATAMENTO
|
||
</button>
|
||
</PageHeader>
|
||
|
||
{treatments && treatments.length === 0 && (
|
||
<div className="bg-white border border-dashed border-gray-300 rounded-xl p-10 text-center">
|
||
<Activity size={32} className="text-gray-300 mx-auto mb-3" />
|
||
<p className="text-sm font-bold text-gray-500 uppercase">NENHUM TRATAMENTO ORTODÔNTICO</p>
|
||
<p className="text-xs text-gray-400 mt-1">CLIQUE EM "NOVO TRATAMENTO" PARA COMEÇAR.</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 gap-6">
|
||
{treatments?.map(t => (
|
||
<div
|
||
key={t.id}
|
||
className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-shadow cursor-pointer"
|
||
onClick={() => setSelectedTreatment(t)}
|
||
>
|
||
<div className="p-4 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center font-bold text-sm">
|
||
{t.pacienteNome.substring(0, 2).toUpperCase()}
|
||
</div>
|
||
<div>
|
||
<h3 className="font-bold text-gray-900 uppercase">{t.pacienteNome}</h3>
|
||
<p className="text-[10px] text-gray-500 uppercase font-bold">
|
||
{t.tipoAparelho} • INÍCIO: {new Date(t.dataInicio).toLocaleDateString()}
|
||
{t.faseAtual && ` • FASE: ${t.faseAtual}`}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
{t.percentualConcluido != null && (
|
||
<div className="hidden md:flex items-center gap-2">
|
||
<div className="w-24 bg-gray-200 rounded-full h-1.5">
|
||
<div className="bg-teal-500 h-1.5 rounded-full" style={{ width: `${t.percentualConcluido}%` }}></div>
|
||
</div>
|
||
<span className="text-[10px] font-bold text-gray-500">{t.percentualConcluido}%</span>
|
||
</div>
|
||
)}
|
||
{t.cooperacao && <span className={`text-[10px] font-bold px-2 py-0.5 rounded-full uppercase hidden lg:inline-block ${COOPERATION_COLORS[t.cooperacao] || ''}`}>{t.cooperacao}</span>}
|
||
<span className={`px-3 py-1 rounded-full text-[10px] font-bold uppercase ${t.status === 'Finalizado' ? 'bg-green-100 text-green-700' : t.status === 'Contenção' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700'}`}>
|
||
{t.status}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview timeline */}
|
||
<div className="p-4">
|
||
<h4 className="text-xs font-bold text-gray-500 mb-3 flex items-center gap-2 uppercase">
|
||
<Activity size={14} /> ÚLTIMAS EVOLUÇÕES
|
||
</h4>
|
||
<div className="space-y-2">
|
||
{t.evolucoes.slice(-2).map((ev) => (
|
||
<div key={ev.id} className="flex items-center gap-3 text-xs">
|
||
<div className="w-2 h-2 bg-teal-500 rounded-full flex-shrink-0"></div>
|
||
<span className="font-bold text-gray-700">{new Date(ev.data).toLocaleDateString()}</span>
|
||
<span className="text-gray-500 truncate uppercase">{ev.procedimento}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Flags preview */}
|
||
{t.flags && t.flags.length > 0 && (
|
||
<div className="flex gap-2 mt-3 pt-3 border-t border-gray-100">
|
||
{t.flags.map((f, i) => {
|
||
const s = FLAG_STYLES[f.tipo] || FLAG_STYLES.info;
|
||
return (
|
||
<span key={i} className={`text-[9px] font-bold px-2 py-1 rounded-lg border uppercase ${s.bg} ${s.text}`}>
|
||
{f.mensagem}
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* Detail panel */}
|
||
{selectedTreatment && (
|
||
<OrthoDetailPanel
|
||
treatment={selectedTreatment}
|
||
onClose={() => { setSelectedTreatment(null); refresh(); }}
|
||
/>
|
||
)}
|
||
|
||
{/* Novo tratamento */}
|
||
{showNovo && (
|
||
<NovoTratamentoModal
|
||
onClose={() => setShowNovo(false)}
|
||
onCreated={() => { setShowNovo(false); refresh(); }}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}; |