import React, { useState, useEffect, useMemo } from 'react'; import { Users, Trash2, Check, AlertCircle, PlusCircle, Camera, Stethoscope, ChevronRight, ChevronLeft, Search, X } from 'lucide-react'; import { PageHeader } from '../components/PageHeader.tsx'; import { ProteticoPicker } from '../components/ProteticoPicker.tsx'; import { SearchSelect } from '../components/SearchSelect.tsx'; import { HybridBackend } from '../services/backend.ts'; import { Paciente, Dentista, Procedimento, Especialidade } from '../types.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { create } from 'zustand'; // --- STORES --- interface GTOItem { id: string; procedimentoId: string; codigoTUSS: string; codigoInterno: string; descricao: string; dente?: string; face?: string; arco?: string; quantidade: number; valorUnitario: number; valorTotal: number; observacao?: string; // Prótese (Fase 1): item ligado ao catálogo do protético → gera OS ao finalizar. produtoId?: string; proteticoId?: string; } interface GTOStore { paciente: Paciente | null; dentista: Dentista | null; credenciada: string; items: GTOItem[]; pendingSearch: string; setPaciente: (p: Paciente | null) => void; setDentista: (d: Dentista | null) => void; setCredenciada: (c: string) => void; addItem: (item: GTOItem) => void; removeItem: (id: string) => void; updateItem: (id: string, update: Partial>) => void; clear: () => void; setPendingSearch: (s: string) => void; } const useGTOStore = create((set) => ({ paciente: null, dentista: null, credenciada: '', items: [], pendingSearch: '', setPaciente: (paciente) => set({ paciente }), setDentista: (dentista) => set({ dentista }), setCredenciada: (credenciada) => set({ credenciada }), addItem: (item) => set((state) => ({ items: [...state.items, item].slice(0, 20) })), removeItem: (id) => set((state) => ({ items: state.items.filter(i => i.id !== id) })), updateItem: (id, update) => set((state) => ({ items: state.items.map(i => i.id === id ? { ...i, ...update } : i) })), clear: () => set({ paciente: null, dentista: null, items: [] }), setPendingSearch: (pendingSearch) => set({ pendingSearch }), })); export { useGTOStore }; // --- ODONTOGRAM DATA --- const DENTES_ADULTO = { q1: [14, 13, 12, 11, 18, 17, 16, 15], q2: [21, 22, 23, 24, 25, 26, 27, 28], q4: [44, 43, 42, 41, 48, 47, 46, 45], q3: [31, 32, 33, 34, 35, 36, 37, 38], extras: { q1: 19, q2: 29, q4: 49, q3: 39 } }; const DENTES_CRIANCA = { q1: [null, 53, 52, 51, null, null, 55, 54], q2: [61, 62, 63, null, 64, 65, null, null], q4: [null, 83, 82, 81, null, null, 85, 84], q3: [71, 72, 73, null, 74, 75, null, null] }; const FACES = ['O', 'M', 'D', 'V', 'L', 'P']; const normalize = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim(); // --- COMPONENT --- export const LancarGTO: React.FC = () => { const store = useGTOStore(); const toast = useToast(); const [pacientes, setPacientes] = useState([]); const [dentistas, setDentistas] = useState([]); const [especialidades, setEspecialidades] = useState([]); const [procedimentos, setProcedimentos] = useState([]); const [convenios, setConvenios] = useState([]); const [selectedEsp, setSelectedEsp] = useState(''); const [selectedProc, setSelectedProc] = useState(null); const [pacienteTipo, setPacienteTipo] = useState<'ADULTO' | 'CRIANCA'>('ADULTO'); const [showMixed, setShowMixed] = useState(false); const [selectedDentes, setSelectedDentes] = useState([]); const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null); const [arcoFace, setArcoFace] = useState(''); const [arcoObs, setArcoObs] = useState(''); const [mobilePage, setMobilePage] = useState<1 | 2 | 3>(1); const [patientSearchOpen, setPatientSearchOpen] = useState(false); const [patientSearchTerm, setPatientSearchTerm] = useState(''); // --- PRÓTESE (Fase 1): catálogo do protético entra como "procedimentos" do GTO --- const [selectedLab, setSelectedLab] = useState(''); const [labProdutos, setLabProdutos] = useState([]); const [selectedProdutoId, setSelectedProdutoId] = useState(''); const [protValor, setProtValor] = useState(''); const [protObs, setProtObs] = useState(''); useEffect(() => { const load = async () => { const p = await HybridBackend.getPacientes(); const d = await HybridBackend.getDentistas(); const es = await HybridBackend.getEspecialidades(); const pr = await HybridBackend.getProcedimentos(); const cv = await HybridBackend.getPlanos(); setPacientes(Array.isArray(p) ? p : (p?.data ?? [])); setDentistas(d ?? []); setEspecialidades(es ?? []); setProcedimentos(pr ?? []); setConvenios(Array.isArray(cv) ? cv : []); }; load(); }, []); const filteredProcedimentos = useMemo(() => { if (!selectedEsp) return []; return procedimentos.filter(p => p.especialidadeId === selectedEsp); }, [selectedEsp, procedimentos]); // Especialidade de prótese → o "procedimento" vem do catálogo do protético (não da // tabela procedimentos). Detecta pela área da especialidade (re-tagueada no backend). const isProtese = useMemo( () => (especialidades.find(e => e.id === selectedEsp)?.area || '') === 'protese', [especialidades, selectedEsp] ); const selectedProduto = useMemo( () => labProdutos.find(p => p.id === selectedProdutoId) || null, [labProdutos, selectedProdutoId] ); // Catálogo do laboratório selecionado. useEffect(() => { if (!selectedLab) { setLabProdutos([]); return; } let active = true; HybridBackend.getLaboratorioProdutos(selectedLab) .then(p => { if (active) setLabProdutos(Array.isArray(p) ? p : []); }) .catch(() => { if (active) setLabProdutos([]); }); return () => { active = false; }; }, [selectedLab]); // Busca de pacientes é SERVER-SIDE: a clínica pode ter milhares e a rota sem termo // devolve só as 50 primeiras. Filtrar essas 50 no cliente fazia a busca "não achar" // quem estava fora da 1ª página. Sem termo, mostra a lista inicial (recentes). const [patientResults, setPatientResults] = useState([]); const [searchingPatients, setSearchingPatients] = useState(false); useEffect(() => { const term = patientSearchTerm.trim(); if (!term) { setPatientResults(pacientes.slice(0, 30)); setSearchingPatients(false); return; } let active = true; setSearchingPatients(true); const t = setTimeout(async () => { try { const r: any = await HybridBackend.getPacientes(term); const list: Paciente[] = Array.isArray(r) ? r : (r?.data ?? []); if (active) setPatientResults(list); } finally { if (active) setSearchingPatients(false); } }, 300); return () => { active = false; clearTimeout(t); }; }, [patientSearchTerm, pacientes]); useEffect(() => { const { pendingSearch } = store; if (!pendingSearch || pacientes.length === 0) return; const term = normalize(pendingSearch); const matches = pacientes.filter(p => { const n = normalize(p.nome); return n === term || n.includes(term) || term.includes(n); }); if (matches.length === 1) { store.setPaciente(matches[0]); } else if (matches.length > 1) { setPatientSearchTerm(pendingSearch); setPatientSearchOpen(true); } else { setPatientSearchOpen(true); } store.setPendingSearch(''); }, [pacientes, store.pendingSearch]); // eslint-disable-line react-hooks/exhaustive-deps // Tooth click → immediate add/remove from store const toggleDente = (num: string) => { if (!selectedProc) { toast.error("SELECIONE UM PROCEDIMENTO ANTES DE ESCOLHER O DENTE."); return; } if (selectedProc.tipo_regiao !== 'DENTE') { toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL."); return; } const existingItem = store.items.find( i => i.dente === num && i.procedimentoId === selectedProc.id ); if (existingItem) { store.removeItem(existingItem.id); setSelectedDentes(prev => prev.filter(d => d !== num)); } else { if (store.items.length >= 20) { toast.error("LIMITE DE 20 ITENS ATINGIDO."); return; } store.addItem({ id: Math.random().toString(36).substring(7), procedimentoId: selectedProc.id, codigoTUSS: selectedProc.codigo || '000000', codigoInterno: selectedProc.codigoInterno || '', descricao: selectedProc.descricao_mobile || selectedProc.nome, dente: num, face: '', quantidade: 1, valorUnitario: selectedProc.valorParticular || 0, valorTotal: selectedProc.valorParticular || 0, }); setSelectedDentes(prev => [...prev, num]); setSelectedArco(null); } }; const selectArco = (arco: 'AI' | 'AS') => { if (selectedProc?.tipo_regiao !== 'ARCO') { toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE ARCO."); return; } setSelectedDentes([]); setSelectedArco(arco); }; // Only used for ARCO / GERAL procedures const handleAddItem = () => { if (!selectedProc) { toast.error("SELECIONE UM PROCEDIMENTO PRIMEIRO."); return; } if (selectedProc.tipo_regiao === 'ARCO' && !selectedArco) { toast.error("SELECIONE UM ARCO (AI/AS)."); return; } if (selectedProc.exige_face && !arcoFace.trim()) { toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO."); return; } store.addItem({ id: Math.random().toString(36).substring(7), procedimentoId: selectedProc.id, codigoTUSS: selectedProc.codigo || '000000', codigoInterno: selectedProc.codigoInterno || '', descricao: selectedProc.nome, arco: selectedArco || undefined, face: arcoFace, quantidade: 1, valorUnitario: selectedProc.valorParticular || 0, valorTotal: selectedProc.valorParticular || 0, observacao: arcoObs || undefined, }); toast.success("ADICIONADO AO RASCUNHO."); setSelectedArco(null); setArcoFace(''); setArcoObs(''); }; // Adiciona um item de prótese (catálogo do protético) ao rascunho do GTO. const handleAddProtese = () => { if (!selectedLab) { toast.error("SELECIONE O LABORATÓRIO."); return; } if (!selectedProduto) { toast.error("SELECIONE O PRODUTO DE PRÓTESE."); return; } if (store.items.length >= 20) { toast.error("LIMITE DE 20 ITENS ATINGIDO."); return; } const valor = parseFloat(protValor) || Number(selectedProduto.preco) || 0; store.addItem({ id: Math.random().toString(36).substring(7), procedimentoId: '', codigoTUSS: '', codigoInterno: '', descricao: selectedProduto.nome, quantidade: 1, valorUnitario: valor, valorTotal: valor, observacao: protObs || undefined, produtoId: selectedProduto.id, proteticoId: selectedLab, }); toast.success("PRÓTESE ADICIONADA AO RASCUNHO."); setSelectedProdutoId(''); setProtValor(''); setProtObs(''); }; const handleFinalize = async () => { if (!store.paciente || !store.dentista || store.items.length === 0) { toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS."); return; } if (!store.credenciada) { toast.error("SELECIONE O CONVÊNIO / CREDENCIADA."); return; } const semFace = store.items.filter(i => { if (!i.dente) return false; const proc = procedimentos.find(p => p.id === i.procedimentoId); return proc?.exige_face && !i.face; }); if (semFace.length > 0) { toast.error(`FACE OBRIGATÓRIA PARA ${semFace.map(i => `DENTE ${i.dente}`).join(', ')}`); return; } try { // Persiste a GTO e finaliza → gera o lançamento (convênio a receber) no Financeiro const { id } = await HybridBackend.createGtoBuilder({ pacienteId: store.paciente.id, dentistaId: store.dentista.id, credenciadaId: store.credenciada, }); for (const it of store.items) { const { arco, id: _localId, produtoId, proteticoId, ...rest } = it as any; void arco; void _localId; const payload: any = { ...rest }; // Prótese: envia produto/protético (snake_case) → o backend abre a OS na finalização. if (produtoId) { payload.produto_id = produtoId; payload.protetico_id = proteticoId; } await HybridBackend.addGtoItem(id, payload); } const fin = await HybridBackend.finalizarGto(id); toast.success(fin?.financeiro_id ? `GTO GERADA — R$ ${totalGeral.toFixed(2)} LANÇADO NO FINANCEIRO (CONVÊNIO A RECEBER).` : "GTO GERADA COM SUCESSO!"); store.clear(); setSelectedEsp(''); setSelectedProc(null); setSelectedDentes([]); setSelectedArco(null); setArcoFace(''); setArcoObs(''); } catch (e: any) { toast.error((e?.message || "ERRO AO GERAR GTO.").toUpperCase()); } }; const totalGeral = store.items.reduce((sum, i) => sum + i.valorTotal, 0); const toggleMixed = () => { if (showMixed) { if (pacienteTipo === 'ADULTO') { const toRemove = selectedDentes.filter(d => parseInt(d) >= 50); toRemove.forEach(d => { const item = store.items.find(i => i.dente === d && i.procedimentoId === selectedProc?.id); if (item) store.removeItem(item.id); }); setSelectedDentes(prev => prev.filter(d => parseInt(d) < 50)); } else { const toRemove = selectedDentes.filter(d => parseInt(d) < 50); toRemove.forEach(d => { const item = store.items.find(i => i.dente === d && i.procedimentoId === selectedProc?.id); if (item) store.removeItem(item.id); }); setSelectedDentes(prev => prev.filter(d => parseInt(d) >= 50)); } } setShowMixed(prev => !prev); }; const renderDenteBtn = (num: number | string | null, keyIdx?: number) => { if (num === null) return
; const sStr = num.toString(); const isDeciduo = parseInt(sStr) >= 50; const isSelected = selectedDentes.includes(sStr); const isDisabled = !!selectedProc && selectedProc.tipo_regiao !== 'DENTE'; return ( ); }; // ─── JSX SECTIONS ─────────────────────────────────────────────── const step1Card = (
01. Paciente
{store.paciente ? (
{store.paciente.nome.charAt(0)}
{store.paciente.nome}
) : ( )}
); const step2Card = (
02. Especialidade
{ setSelectedEsp(v); setSelectedProc(null); setSelectedDentes([]); setSelectedArco(null); setSelectedProdutoId(''); setProtValor(''); setProtObs(''); }} options={especialidades.map(es => ({ value: es.id, label: es.nome }))} placeholder="ESCOLHA A ÁREA..." title="Especialidade" /> {selectedEsp && isProtese && (
{ setSelectedLab(id); setSelectedProdutoId(''); setProtValor(''); }} /> {selectedLab && (labProdutos.length === 0 ? (

Este laboratório não tem produtos ativos

) : ( <> { setSelectedProdutoId(v); const pr = labProdutos.find(p => p.id === v); setProtValor(pr ? String(pr.preco ?? '') : ''); }} options={labProdutos.map(p => ({ value: p.id, label: p.nome, hint: `R$ ${Number(p.preco || 0).toFixed(2)}` }))} placeholder="ESCOLHA A PRÓTESE..." title="Produto do laboratório" className="w-full p-4 bg-white border-2 border-teal-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase" /> {selectedProduto && ( <>
setProtValor(e.target.value)} className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-teal-400 outline-none transition-all" placeholder="0,00" />