diff --git a/frontend/views/LancarGTO.tsx b/frontend/views/LancarGTO.tsx
index 3285961..a1d6651 100644
--- a/frontend/views/LancarGTO.tsx
+++ b/frontend/views/LancarGTO.tsx
@@ -1,713 +1,877 @@
-import React, { useState, useEffect, useMemo } from 'react';
-import {
- Users,
- Trash2,
- Check,
- Building2,
- AlertCircle,
- PlusCircle,
- Camera,
- Stethoscope,
- ChevronRight,
- Search,
- X
-} from 'lucide-react';
-import { PageHeader } from '../components/PageHeader.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;
-}
-
-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;
- clear: () => void;
- setPendingSearch: (s: string) => void;
-}
-
-const useGTOStore = create
((set) => ({
- paciente: null,
- dentista: null,
- credenciada: 'CASSEMS - SEDE CENTRAL',
- 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)
- })),
- clear: () => set({ paciente: null, dentista: null, items: [] }),
- setPendingSearch: (pendingSearch) => set({ pendingSearch }),
-}));
-
-export { useGTOStore };
-
-// --- ODONTOGRAM DATA (4x2 Grid Layout) ---
-const DENTES_ADULTO = {
- q1: [14, 13, 12, 11, 18, 17, 16, 15], // Top: 14-11, Bottom: 18-15
- q2: [21, 22, 23, 24, 25, 26, 27, 28], // Top: 21-24, Bottom: 25-28
- q4: [44, 43, 42, 41, 48, 47, 46, 45], // Top: 44-41, Bottom: 48-45
- q3: [31, 32, 33, 34, 35, 36, 37, 38], // Top: 31-34, Bottom: 35-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], // fix: eram 43-46 (permanentes)
- q3: [71, 72, 73, null, 74, 75, null, null]
-};
-
-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 [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 [face, setFace] = useState('');
- const [obs, setObs] = useState('');
- const [patientSearchOpen, setPatientSearchOpen] = useState(false);
- const [patientSearchTerm, setPatientSearchTerm] = 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();
- setPacientes(Array.isArray(p) ? p : (p?.data ?? []));
- setDentistas(d ?? []);
- setEspecialidades(es ?? []);
- setProcedimentos(pr ?? []);
- };
- load();
- }, []);
-
- const filteredProcedimentos = useMemo(() => {
- if (!selectedEsp) return [];
- return procedimentos.filter(p => p.especialidadeId === selectedEsp);
- }, [selectedEsp, procedimentos]);
-
- const filteredPatients = useMemo(() => {
- if (!patientSearchTerm) return pacientes.slice(0, 30);
- const term = normalize(patientSearchTerm);
- return pacientes.filter(p =>
- normalize(p.nome).includes(term) ||
- (p.cpf && p.cpf.includes(patientSearchTerm.replace(/\D/g, '')))
- ).slice(0, 30);
- }, [pacientes, patientSearchTerm]); // eslint-disable-line react-hooks/exhaustive-deps
-
- // Auto-seleciona paciente quando vem da agenda (pendingSearch)
- 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
-
- const toggleDente = (num: string) => {
- if (selectedProc?.tipo_regiao !== 'DENTE') {
- toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL.");
- return;
- }
- setSelectedArco(null);
- setSelectedDentes(prev =>
- prev.includes(num) ? prev.filter(d => d !== num) : [...prev, num]
- );
- };
-
- 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);
- };
-
- const handleAddItem = () => {
- if (!selectedProc) {
- toast.error("SELECIONE UM PROCEDIMENTO PRIMEIRO.");
- return;
- }
-
- // Validação de Região
- 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).");
- return;
- }
-
- // Validação de Face
- if (selectedProc.exige_face && (!face || face.trim() === '')) {
- toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO.");
- return;
- }
-
- const newItem: GTOItem = {
- id: Math.random().toString(36).substring(7),
- procedimentoId: selectedProc.id,
- codigoTUSS: selectedProc.codigo || '000000',
- codigoInterno: selectedProc.codigoInterno || '',
- descricao: selectedProc.nome,
- dente: selectedDentes.join(', '),
- arco: selectedArco || undefined,
- face: face,
- quantidade: 1,
- valorUnitario: selectedProc.valorParticular || 0,
- valorTotal: selectedProc.valorParticular || 0,
- observacao: obs,
- };
-
- store.addItem(newItem);
- toast.success("ADICIONADO AO RASCUNHO.");
-
- // Reset selection fields
- setSelectedDentes([]);
- setSelectedArco(null);
- setFace('');
- setObs('');
- };
-
- 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;
- }
-
- try {
- toast.success("GTO GERADA COM SUCESSO!");
- store.clear();
- setSelectedEsp('');
- setSelectedProc(null);
- } catch {
- toast.error("ERRO AO GERAR GTO.");
- }
- };
-
- const totalGeral = store.items.reduce((sum, i) => sum + i.valorTotal, 0);
-
- const toggleMixed = () => {
- if (showMixed) {
- // Ao fechar: remove os dentes do tipo expandido da seleção
- if (pacienteTipo === 'ADULTO') {
- setSelectedDentes(prev => prev.filter(d => parseInt(d) < 50));
- } else {
- 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 (
-
- );
- };
-
- return (
-
-
-
-
-
- {/* LEFT PANEL: ODONTOGRAM & ITEMS */}
-
-
- {/* ODONTOGRAM BOX */}
-
-
-
-
-
-
-
-
- {/* Dynamic Odontogram Grid */}
-
-
-
-
-
- {/* Q1 & Q2 (SUPERIOR) */}
-
- {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
-
- {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q1 : DENTES_CRIANCA.q1).map((d, i) => renderDenteBtn(d, i))}
-
-
-
-
- {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q2 : DENTES_CRIANCA.q2).map((d, i) => renderDenteBtn(d, i))}
-
- {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
-
-
- {/* Q4 & Q3 (INFERIOR) */}
-
- {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
-
- {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q4 : DENTES_CRIANCA.q4).map((d, i) => renderDenteBtn(d, i))}
-
-
-
-
- {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q3 : DENTES_CRIANCA.q3).map((d, i) => renderDenteBtn(d, i))}
-
- {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
-
-
-
-
- {/* SEÇÃO MISTO — expande abaixo do grid principal */}
- {showMixed && (
-
-
- {pacienteTipo === 'ADULTO' ? '— DENTES DECÍDUOS —' : '— DENTES PERMANENTES —'}
-
-
-
-
-
- {/* Q1 & Q2 MISTO */}
-
- {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
-
- {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q1 : DENTES_ADULTO.q1).map((d, i) => renderDenteBtn(d, i))}
-
-
-
-
- {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q2 : DENTES_ADULTO.q2).map((d, i) => renderDenteBtn(d, i))}
-
- {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
-
- {/* Q4 & Q3 MISTO */}
-
- {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
-
- {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q4 : DENTES_ADULTO.q4).map((d, i) => renderDenteBtn(d, i))}
-
-
-
-
- {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q3 : DENTES_ADULTO.q3).map((d, i) => renderDenteBtn(d, i))}
-
- {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
-
-
-
-
- )}
-
- {/* BOTÃO EXPANSOR — dentição mista */}
-
-
-
-
-
-
-
-
- {/* RASCUNHO LIST */}
-
-
-
PROCEDIMENTOS DO RASCUNHO
-
{store.items.length}/20 ITENS
-
-
-
- {store.items.length === 0 && (
-
-
- NÃO HÁ ITENS ADICIONADOS
-
- )}
-
- {store.items.map((item) => (
-
-
-
-
- COD {item.codigoTUSS}
-
{item.descricao}
-
-
- {store.paciente?.nome}
-
-
-
-
-
-
-
DENTE: {item.dente || '--'}
-
ARCO: {item.arco || '--'}
-
FACE: {item.face || '--'}
-
VALOR: R$ {item.valorTotal.toFixed(2)}
-
-
- {item.observacao && (
-
- OBS: {item.observacao}
-
- )}
-
- ))}
-
-
-
-
-
TOTAL ACUMULADO
-
R$ {totalGeral.toFixed(2)}
-
-
-
-
-
-
- {/* RIGHT PANEL: WORKFLOW SELECTORS */}
-
-
- {/* STEP 1: PACIENTE */}
-
-
- 01. Paciente
-
- {store.paciente ? (
-
-
-
- {store.paciente.nome.charAt(0)}
-
-
{store.paciente.nome}
-
-
-
- ) : (
-
- )}
-
-
- {/* STEP 2: ESPECIALIDADE & PROCEDIMENTO */}
-
-
- 02. Especialidade
-
-
-
-
- {selectedEsp && (
-
-
-
- {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 CLINICA
-
- )}
-
-
- )}
-
- )}
-
-
-
- {/* STEP 3: DENTISTA */}
-
-
- 03. Dentista Executor
-
-
-
-
- {/* STEP 4: DETALHES (FACE/OBS) */}
-
-
-
-
- setFace(e.target.value.toUpperCase())}
- disabled={selectedProc?.tipo_regiao === 'GERAL' && !selectedProc?.exige_face}
- 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 ? "DIGITE AS FACES..." : "OPCIONAL"}
- />
-
-
-
-
-
-
-
-
- {/* ACTIONS */}
-
-
-
-
-
-
-
-
-
- {/* MODAL DE BUSCA DE PACIENTE */}
- {patientSearchOpen && (
-
-
-
-
- BUSCAR PACIENTE
-
-
-
-
-
setPatientSearchTerm(e.target.value.toUpperCase())}
- placeholder="NOME OU CPF..."
- className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-blue-500 outline-none transition-all uppercase"
- />
-
- {filteredPatients.length === 0 ? (
-
- {patientSearchTerm ? 'NENHUM PACIENTE ENCONTRADO' : 'CARREGANDO...'}
-
- ) : (
- filteredPatients.map(p => (
-
- ))
- )}
-
-
-
-
- )}
-
- );
-};
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+ Users,
+ Trash2,
+ Check,
+ Building2,
+ AlertCircle,
+ PlusCircle,
+ Camera,
+ Stethoscope,
+ ChevronRight,
+ ChevronLeft,
+ Search,
+ X
+} from 'lucide-react';
+import { PageHeader } from '../components/PageHeader.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;
+}
+
+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;
+ clear: () => void;
+ setPendingSearch: (s: string) => void;
+}
+
+const useGTOStore = create((set) => ({
+ paciente: null,
+ dentista: null,
+ credenciada: 'CASSEMS - SEDE CENTRAL',
+ 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)
+ })),
+ clear: () => set({ paciente: null, dentista: null, items: [] }),
+ setPendingSearch: (pendingSearch) => set({ pendingSearch }),
+}));
+
+export { useGTOStore };
+
+// --- ODONTOGRAM DATA (4x2 Grid Layout) ---
+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 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 [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);
+
+ // Per-tooth face/obs
+ const [denteInputs, setDenteInputs] = useState>({});
+ // Arco/Geral face/obs (single)
+ 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('');
+
+ useEffect(() => {
+ const load = async () => {
+ const p = await HybridBackend.getPacientes();
+ const d = await HybridBackend.getDentistas();
+ const es = await HybridBackend.getEspecialidades();
+ const pr = await HybridBackend.getProcedimentos();
+ setPacientes(Array.isArray(p) ? p : (p?.data ?? []));
+ setDentistas(d ?? []);
+ setEspecialidades(es ?? []);
+ setProcedimentos(pr ?? []);
+ };
+ load();
+ }, []);
+
+ const filteredProcedimentos = useMemo(() => {
+ if (!selectedEsp) return [];
+ return procedimentos.filter(p => p.especialidadeId === selectedEsp);
+ }, [selectedEsp, procedimentos]);
+
+ const filteredPatients = useMemo(() => {
+ if (!patientSearchTerm) return pacientes.slice(0, 30);
+ const term = normalize(patientSearchTerm);
+ return pacientes.filter(p =>
+ normalize(p.nome).includes(term) ||
+ (p.cpf && p.cpf.includes(patientSearchTerm.replace(/\D/g, '')))
+ ).slice(0, 30);
+ }, [pacientes, patientSearchTerm]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ 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
+
+ const resetProcSelection = () => {
+ setSelectedDentes([]);
+ setSelectedArco(null);
+ setDenteInputs({});
+ setArcoFace('');
+ setArcoObs('');
+ };
+
+ const toggleDente = (num: string) => {
+ 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; });
+ setSelectedDentes(prev => prev.filter(d => d !== num));
+ } else {
+ setDenteInputs(di => ({ ...di, [num]: { face: '', obs: '' } }));
+ setSelectedDentes(prev => [...prev, num]);
+ }
+ };
+
+ const selectArco = (arco: 'AI' | 'AS') => {
+ if (selectedProc?.tipo_regiao !== 'ARCO') {
+ toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE ARCO.");
+ return;
+ }
+ setSelectedDentes([]);
+ setDenteInputs({});
+ setSelectedArco(arco);
+ };
+
+ 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).");
+ 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.");
+ }
+
+ resetProcSelection();
+ };
+
+ 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;
+ }
+ try {
+ toast.success("GTO GERADA COM SUCESSO!");
+ store.clear();
+ setSelectedEsp('');
+ setSelectedProc(null);
+ resetProcSelection();
+ } catch {
+ toast.error("ERRO AO GERAR GTO.");
+ }
+ };
+
+ 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);
+ setDenteInputs(di => { const next = { ...di }; toRemove.forEach(d => delete next[d]); return next; });
+ 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; });
+ 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
+
+
+
+
+ {selectedEsp && (
+
+
+
+ {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
+
+ )}
+
+
+ )}
+
+ )}
+
+
+ );
+
+ const step3Card = (
+
+
+ 03. Dentista Executor
+
+
+
+ );
+
+ // Per-tooth inputs (DENTE procedure) or unified (ARCO/GERAL)
+ const isPerTooth = selectedProc?.tipo_regiao === 'DENTE' && selectedDentes.length > 0;
+ const step4Card = (
+
+
+
+ 04. {isPerTooth ? `Face / Obs por Dente (${selectedDentes.length})` : 'Face & Observações'}
+
+ {selectedProc?.exige_face && (
+
* OBRIGATÓRIO
+ )}
+
+
+ {isPerTooth ? (
+
+ {selectedDentes.map(dente => {
+ const isDeciduo = parseInt(dente) >= 50;
+ const inp = denteInputs[dente] || { face: '', obs: '' };
+ return (
+
+ );
+ })}
+
+ ) : (
+
+
+
+ setArcoFace(e.target.value.toUpperCase())}
+ disabled={selectedProc?.tipo_regiao === 'GERAL' && !selectedProc?.exige_face}
+ 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 ? "DIGITE AS FACES..." : "OPCIONAL"}
+ />
+
+
+
+
+
+
+ )}
+
+ );
+
+ const actionsDesktop = (
+
+
+
+
+ );
+
+ const actionsMobile = (
+
+ );
+
+ const odontogramCard = (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q1 : DENTES_CRIANCA.q1).map((d, i) => renderDenteBtn(d, i))}
+
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q2 : DENTES_CRIANCA.q2).map((d, i) => renderDenteBtn(d, i))}
+
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
+
+
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q4 : DENTES_CRIANCA.q4).map((d, i) => renderDenteBtn(d, i))}
+
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q3 : DENTES_CRIANCA.q3).map((d, i) => renderDenteBtn(d, i))}
+
+ {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
+
+
+
+
+ {showMixed && (
+
+
+ {pacienteTipo === 'ADULTO' ? '— DENTES DECÍDUOS —' : '— DENTES PERMANENTES —'}
+
+
+
+
+
+
+ {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q1 : DENTES_ADULTO.q1).map((d, i) => renderDenteBtn(d, i))}
+
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q2 : DENTES_ADULTO.q2).map((d, i) => renderDenteBtn(d, i))}
+
+ {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
+
+
+ {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q4 : DENTES_ADULTO.q4).map((d, i) => renderDenteBtn(d, i))}
+
+
+
+
+ {(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q3 : DENTES_ADULTO.q3).map((d, i) => renderDenteBtn(d, i))}
+
+ {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+
+ const rascunhoCard = (
+
+
+
PROCEDIMENTOS DO RASCUNHO
+
{store.items.length}/20 ITENS
+
+
+ {store.items.length === 0 && (
+
+
+ NÃO HÁ ITENS ADICIONADOS
+
+ )}
+ {store.items.map((item) => (
+
+
+
+
+ COD {item.codigoTUSS}
+
{item.descricao}
+
+
+ {store.paciente?.nome}
+
+
+
+
+
+
DENTE: {item.dente || '--'}
+
ARCO: {item.arco || '--'}
+
FACE: {item.face || '--'}
+
VALOR: R$ {item.valorTotal.toFixed(2)}
+
+ {item.observacao && (
+
+ OBS: {item.observacao}
+
+ )}
+
+ ))}
+
+
+
+
TOTAL ACUMULADO
+
R$ {totalGeral.toFixed(2)}
+
+
+
+
+ );
+
+ // ─── RENDER ─────────────────────────────────────────────────────
+
+ return (
+
+
+
+ {/* ─── DESKTOP LAYOUT ─────────────────────────────── */}
+
+
+ {odontogramCard}
+ {rascunhoCard}
+
+
+ {step1Card}
+ {step2Card}
+ {step3Card}
+ {step4Card}
+ {actionsDesktop}
+
+
+
+ {/* ─── MOBILE LAYOUT (3 páginas) ──────────────────── */}
+
+ {mobilePage === 1 && (
+
+ {step1Card}
+ {step2Card}
+ {step3Card}
+
+ )}
+ {mobilePage === 2 && odontogramCard}
+ {mobilePage === 3 && (
+
+ {step4Card}
+ {actionsMobile}
+ {rascunhoCard}
+
+ )}
+
+
+ {/* ─── MOBILE FOOTER NAV ──────────────────────────── */}
+
+ {mobilePage > 1 ? (
+
+ ) : (
+
+ )}
+
+
+ {([1, 2, 3] as const).map(p => (
+
+
+ {mobilePage < 3 ? (
+
+ ) : (
+
+ )}
+
+
+ {/* ─── PATIENT SEARCH MODAL ───────────────────────── */}
+ {patientSearchOpen && (
+
+
+
+
+ BUSCAR PACIENTE
+
+
+
+
+
setPatientSearchTerm(e.target.value.toUpperCase())}
+ placeholder="NOME OU CPF..."
+ className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-blue-500 outline-none transition-all uppercase"
+ />
+
+ {filteredPatients.length === 0 ? (
+
+ {patientSearchTerm ? 'NENHUM PACIENTE ENCONTRADO' : 'CARREGANDO...'}
+
+ ) : (
+ filteredPatients.map(p => (
+
+ ))
+ )}
+
+
+
+
+ )}
+
+ );
+};