import React, { useState, useEffect, useCallback } from 'react'; import { FlaskConical, Building2, Clock, ShieldCheck, AlertTriangle, CheckCircle2, Circle, Package, History, Camera, MapPin, ChevronRight, Loader2, ImagePlus, Zap, Briefcase, Wallet, ChevronLeft } from 'lucide-react'; import { HybridBackend } from '../services/backend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 }); const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/'); const dataHoraBR = (d: string) => { if (!d) return ''; const x = new Date(d); return isNaN(x.getTime()) ? dataBR(d) : x.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }); }; const STATUS_LABEL: Record = { solicitado: 'Solicitado', recebido: 'Recebido', producao: 'Em produção', prova: 'Prova', ajuste: 'Ajuste', finalizado: 'Finalizado', entregue: 'Entregue', cancelado: 'Cancelado' }; const STATUS_COR: Record = { solicitado: 'bg-gray-100 text-gray-600', recebido: 'bg-blue-100 text-blue-700', producao: 'bg-amber-100 text-amber-700', prova: 'bg-indigo-100 text-indigo-700', ajuste: 'bg-orange-100 text-orange-700', finalizado: 'bg-teal-100 text-teal-700', entregue: 'bg-green-100 text-green-700', cancelado: 'bg-red-100 text-red-600' }; const FLUXO = ['solicitado', 'recebido', 'producao', 'prova', 'ajuste', 'finalizado', 'entregue']; const OCOR_CATS: Record = { nao_devolvido: 'Componente não devolvido', danificado: 'Componente danificado', incompativel: 'Componente incompatível', parafuso_substituido: 'Parafuso substituído', parafuso_desgastado: 'Parafuso desgastado', peca_incorreta: 'Peça incorreta', ajuste_inadequado: 'Ajuste inadequado', falha_fabricacao: 'Falha de fabricação', retrabalho: 'Retrabalho', outro: 'Outro' }; const Secao: React.FC<{ titulo: string; Icon: any; children: React.ReactNode }> = ({ titulo, Icon, children }) => (

{titulo}

{children}
); const PROD_FLUXO = ['recebido', 'producao', 'prova', 'ajuste', 'finalizado']; const mesBR = (m: string) => { const [y, mo] = (m || '').split('-'); return mo ? new Date(Number(y), Number(mo) - 1, 1).toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }) : m; }; // Carteira do protético (via link): trabalhos + mini financeiro mensal (relação com a clínica). const CarteiraView: React.FC<{ carteira: any }> = ({ carteira }) => { if (!carteira) return
; const Linha: React.FC<{ t: any; fin?: boolean }> = ({ t, fin }) => (
{STATUS_LABEL[t.status] || t.status}

{t.tipo}

{t.paciente_nome || '—'}{!fin && t.prazo_entrega ? ` · prazo ${dataBR(t.prazo_entrega)}` : ''}

{Number(t.custo) > 0 && {fmtBRL(t.custo)}}
); return (
{carteira.clinica_nome &&
{carteira.clinica_nome}
} {/* Mini financeiro */}

Recebido desta clínica

{fmtBRL(carteira.financeiro?.total_pago || 0)}

{(carteira.financeiro?.por_mes || []).map((m: any) => (
{mesBR(m.mes)} {fmtBRL(m.total)}
))} {(carteira.financeiro?.por_mes || []).length === 0 &&

Nenhum pagamento registrado ainda.

}
{(carteira.andamento || []).map((t: any) => )} {(carteira.andamento || []).length === 0 &&

Nada em produção.

}
{(carteira.finalizados || []).map((t: any) => )} {(carteira.finalizados || []).length === 0 &&

Nenhum entregue ainda.

}

Valores recebidos do laboratório · sem dados do paciente

); }; export const ProtesePublicView: React.FC<{ token: string }> = ({ token }) => { const toast = useToast(); const [os, setOs] = useState(null); const [erro, setErro] = useState(''); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [tela, setTela] = useState<'os' | 'carteira'>('os'); const [carteira, setCarteira] = useState(null); const recarregar = useCallback(() => HybridBackend.getProtesePublic(token).then(setOs).catch(e => setErro(e.message || 'Link inválido.')), [token]); useEffect(() => { recarregar().finally(() => setLoading(false)); }, [recarregar]); const abrirCarteira = () => { setTela('carteira'); if (!carteira) HybridBackend.getCarteiraPublic(token).then(setCarteira).catch(() => setCarteira({ andamento: [], finalizados: [], financeiro: { por_mes: [], total_pago: 0 } })); }; const act = async (fn: () => Promise, ok: string) => { setBusy(true); try { await fn(); toast.success(ok); await recarregar(); } catch (e: any) { toast.error((e.message || 'ERRO').toUpperCase()); } finally { setBusy(false); } }; const receber = () => act(() => HybridBackend.custodiaPublic(token, 'laboratorio'), 'RECEBIMENTO CONFIRMADO.'); const devolver = () => act(() => HybridBackend.custodiaPublic(token, 'clinica'), 'DEVOLUÇÃO REGISTRADA.'); const avancar = (s: string) => act(() => HybridBackend.statusPublic(token, s), 'ETAPA ATUALIZADA.'); const anexar = (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (!f) return; act(() => HybridBackend.uploadFotoPublic(token, f), 'FOTO ANEXADA.'); e.target.value = ''; }; if (loading) return
; if (erro || !os) return (

{erro || 'Link inválido'}

Peça um novo link à clínica.

); const noLab = (os.local_atual || 'clinica') === 'laboratorio'; const idxAtual = FLUXO.indexOf(os.status); const cancelado = os.status === 'cancelado'; return (
{/* Topo / marca */}

ScoreOdonto

Acompanhamento de prótese

{tela === 'os' ? : }
{tela === 'carteira' && } {tela === 'os' &&
{/* Cabeçalho da OS */}

{os.tipo}

OS {os.id}

{STATUS_LABEL[os.status] || os.status}
{os.clinica_nome && {os.clinica_nome}} {noLab ? 'Com o laboratório' : 'Na clínica'} {os.tipo_os === 'garantia' && Garantia}
{/* Ações do protético (sem conta) */} {!os.finalizada && (
{noLab ? : }

Etapa de produção

{PROD_FLUXO.map(s => ( ))}
)} {/* Etapas */} {cancelado ? (
OS cancelada
) : (
{FLUXO.map((s, i) => { const done = i < idxAtual, atual = i === idxAtual; const ev = (os.eventos || []).find((e: any) => e.status === s); return (
{done ? : atual ?
: } {STATUS_LABEL[s]} {ev ? dataBR(ev.created_at) : ''}
{i < FLUXO.length - 1 &&
} ); })}
)} {/* Dados */}
{os.paciente_nome &&
Paciente{os.paciente_nome}
} {os.dentista_nome &&
Dentista{os.dentista_nome}
} {os.dentes &&
Dentes{os.dentes}
} {os.material &&
Material{os.material}
} {os.cor &&
Cor{os.cor}
} {os.prazo_entrega &&
Prazo{dataBR(os.prazo_entrega)}
} {Number(os.custo) > 0 &&
Valor do lab{fmtBRL(os.custo)}
}
{os.observacoes &&

{os.observacoes}

} {os.garantia_ate &&
Garantia até {dataBR(os.garantia_ate)}
}
{/* Componentes */} {(os.componentes || []).length > 0 && (
{os.componentes.map((c: any) => (
{c.direcao === 'devolvido' ? 'Devolvido' : 'Enviado'} {c.quantidade}× {c.nome}
))}
)} {/* Fotos */} {(os.fotos || []).length > 0 && (
{os.fotos.map((f: any) => ( {f.legenda &&

{f.legenda}

}
))}
)} {/* Ocorrências */} {(os.ocorrencias || []).length > 0 && (
{os.ocorrencias.map((o: any) => (
{(o.status || '').replace('_', ' ')} {OCOR_CATS[o.categoria] || o.categoria}

{o.descricao}

{o.resposta &&

Resposta: {o.resposta}

}
))}
)} {/* Histórico */}
{(os.eventos || []).map((ev: any, i: number) => (
{ev.observacao || STATUS_LABEL[ev.status] || ev.status} {dataHoraBR(ev.created_at)}{ev.actor_nome ? ` · ${ev.actor_nome}` : ''}
))}
{/* CTA de conversão */}

Gostou de acompanhar por aqui?

Crie sua conta grátis e gerencie seu laboratório: bancada, etapas, financeiro e score.

Criar conta grátis

ScoreOdonto · Provedor de Prótese

}
); };