diff --git a/frontend/views/LancarGTO.tsx b/frontend/views/LancarGTO.tsx index a1d6651..864867d 100644 --- a/frontend/views/LancarGTO.tsx +++ b/frontend/views/LancarGTO.tsx @@ -3,7 +3,6 @@ import { Users, Trash2, Check, - Building2, AlertCircle, PlusCircle, Camera, @@ -46,6 +45,7 @@ interface GTOStore { setCredenciada: (c: string) => void; addItem: (item: GTOItem) => void; removeItem: (id: string) => void; + updateItem: (id: string, update: Partial>) => void; clear: () => void; setPendingSearch: (s: string) => void; } @@ -65,13 +65,16 @@ const useGTOStore = create((set) => ({ 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 (4x2 Grid Layout) --- +// --- ODONTOGRAM DATA --- const DENTES_ADULTO = { q1: [14, 13, 12, 11, 18, 17, 16, 15], q2: [21, 22, 23, 24, 25, 26, 27, 28], @@ -87,6 +90,8 @@ const DENTES_CRIANCA = { 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(); @@ -106,10 +111,6 @@ export const LancarGTO: React.FC = () => { const [showMixed, setShowMixed] = useState(false); const [selectedDentes, setSelectedDentes] = useState([]); const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null); - - // Per-tooth face/obs - const [denteInputs, setDenteInputs] = useState>({}); - // Arco/Geral face/obs (single) const [arcoFace, setArcoFace] = useState(''); const [arcoObs, setArcoObs] = useState(''); @@ -164,26 +165,43 @@ export const LancarGTO: React.FC = () => { store.setPendingSearch(''); }, [pacientes, store.pendingSearch]); // eslint-disable-line react-hooks/exhaustive-deps - const resetProcSelection = () => { - setSelectedDentes([]); - setSelectedArco(null); - setDenteInputs({}); - setArcoFace(''); - setArcoObs(''); - }; - + // Tooth click → immediate add/remove from store const toggleDente = (num: string) => { - if (selectedProc?.tipo_regiao !== 'DENTE') { + 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; } - setSelectedArco(null); - if (selectedDentes.includes(num)) { - setDenteInputs(di => { const next = { ...di }; delete next[num]; return next; }); + + 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 { - setDenteInputs(di => ({ ...di, [num]: { face: '', obs: '' } })); + 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); } }; @@ -193,73 +211,40 @@ export const LancarGTO: React.FC = () => { return; } setSelectedDentes([]); - setDenteInputs({}); setSelectedArco(arco); }; + // Only used for ARCO / GERAL procedures const handleAddItem = () => { if (!selectedProc) { toast.error("SELECIONE UM PROCEDIMENTO PRIMEIRO."); return; } - - if (selectedProc.tipo_regiao === 'DENTE' && selectedDentes.length === 0) { - toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE AO MENOS UM DENTE."); - return; - } - if (selectedProc.tipo_regiao === 'ARCO' && !selectedArco) { - toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE UM ARCO (AI/AS)."); + toast.error("SELECIONE UM ARCO (AI/AS)."); return; } - - if (selectedProc.tipo_regiao === 'DENTE') { - if (selectedProc.exige_face) { - const semFace = selectedDentes.filter(d => !denteInputs[d]?.face?.trim()); - if (semFace.length > 0) { - toast.error(`FACE OBRIGATÓRIA PARA: ${semFace.join(', ')}`); - return; - } - } - selectedDentes.forEach(dente => { - const inp = denteInputs[dente] || { face: '', obs: '' }; - store.addItem({ - id: Math.random().toString(36).substring(7), - procedimentoId: selectedProc.id, - codigoTUSS: selectedProc.codigo || '000000', - codigoInterno: selectedProc.codigoInterno || '', - descricao: selectedProc.nome, - dente, - face: inp.face, - quantidade: 1, - valorUnitario: selectedProc.valorParticular || 0, - valorTotal: selectedProc.valorParticular || 0, - observacao: inp.obs || undefined, - }); - }); - toast.success(`${selectedDentes.length} ITEM(S) ADICIONADO(S) AO RASCUNHO.`); - } else { - 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."); + if (selectedProc.exige_face && !arcoFace.trim()) { + toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO."); + return; } - - resetProcSelection(); + 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(''); }; const handleFinalize = async () => { @@ -267,12 +252,24 @@ export const LancarGTO: React.FC = () => { toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS."); 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 { toast.success("GTO GERADA COM SUCESSO!"); store.clear(); setSelectedEsp(''); setSelectedProc(null); - resetProcSelection(); + setSelectedDentes([]); + setSelectedArco(null); + setArcoFace(''); + setArcoObs(''); } catch { toast.error("ERRO AO GERAR GTO."); } @@ -284,11 +281,17 @@ export const LancarGTO: React.FC = () => { if (showMixed) { if (pacienteTipo === 'ADULTO') { const toRemove = selectedDentes.filter(d => parseInt(d) >= 50); - setDenteInputs(di => { const next = { ...di }; toRemove.forEach(d => delete next[d]); return next; }); + 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); - setDenteInputs(di => { const next = { ...di }; toRemove.forEach(d => delete next[d]); return next; }); + 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)); } } @@ -339,18 +342,14 @@ export const LancarGTO: React.FC = () => { {store.paciente.nome} - ) : ( - )} @@ -368,7 +367,8 @@ export const LancarGTO: React.FC = () => { onChange={(e) => { setSelectedEsp(e.target.value); setSelectedProc(null); - resetProcSelection(); + setSelectedDentes([]); + setSelectedArco(null); }} className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-700 text-sm focus:border-orange-400 outline-none transition-all uppercase" > @@ -383,7 +383,10 @@ export const LancarGTO: React.FC = () => { onChange={(e) => { const pr = procedimentos.find(p => p.id === e.target.value); setSelectedProc(pr || null); - resetProcSelection(); + setSelectedDentes([]); + setSelectedArco(null); + setArcoFace(''); + setArcoObs(''); }} className="w-full p-4 bg-white border-2 border-orange-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase" > @@ -392,19 +395,20 @@ export const LancarGTO: React.FC = () => { {selectedProc && ( -
-

REGRAS DE SELEÇÃO

-
- - - {selectedProc.tipo_regiao === 'DENTE' ? 'EXIGE DENTE(S)' : selectedProc.tipo_regiao === 'ARCO' ? 'EXIGE ARCO (AI/AS)' : 'PROCEDIMENTO GERAL'} - - {selectedProc.exige_face && ( - - EXIGE FACE CLÍNICA - - )} +
+
+ + {selectedProc.tipo_regiao === 'DENTE' + ? 'CLIQUE OS DENTES NO ODONTOGRAMA' + : selectedProc.tipo_regiao === 'ARCO' + ? 'SELECIONE O ARCO (AI/AS)' + : 'PROCEDIMENTO GERAL — CLIQUE ADICIONAR'}
+ {selectedProc.exige_face && ( +
+ EXIGE FACE CLÍNICA +
+ )}
)}
@@ -432,122 +436,52 @@ export const LancarGTO: React.FC = () => {
); - // Per-tooth inputs (DENTE procedure) or unified (ARCO/GERAL) - const isPerTooth = selectedProc?.tipo_regiao === 'DENTE' && selectedDentes.length > 0; - const step4Card = ( + // Step 4: only for ARCO / GERAL (face + obs + ADICIONAR button) + const step4Card = selectedProc && selectedProc.tipo_regiao !== 'DENTE' ? (
-

- 04. {isPerTooth ? `Face / Obs por Dente (${selectedDentes.length})` : 'Face & Observações'} -

- {selectedProc?.exige_face && ( - * OBRIGATÓRIO - )} +

04. Face & Observações

+ {selectedProc.exige_face && * OBRIGATÓRIO} +
+
+ + setArcoFace(e.target.value.toUpperCase())} + className={`w-full p-4 border-2 rounded-2xl text-sm font-black outline-none transition-all + ${selectedProc.exige_face ? 'bg-red-50 border-red-100 focus:border-red-400' : 'bg-gray-50 border-gray-100 focus:border-amber-400'}`} + placeholder={selectedProc.exige_face ? "FACE OBRIGATÓRIA..." : "OPCIONAL"} + /> +
+
+ +