merge: faces/obs por dente + descricao_mobile + layout mobile 3 slides

This commit is contained in:
VPS 4 Builder
2026-05-15 04:43:56 +02:00
3 changed files with 885 additions and 713 deletions
+1
View File
@@ -241,6 +241,7 @@ export interface Procedimento {
codigo?: string; codigo?: string;
codigoInterno?: string; codigoInterno?: string;
descricao?: string; descricao?: string;
descricao_mobile?: string;
especialidadeId: string; especialidadeId: string;
especialidadeNome: string; especialidadeNome: string;
valorParticular: number; valorParticular: number;
+7
View File
@@ -457,6 +457,7 @@ export const ProcedimentosView: React.FC = () => {
codigoInterno: data.codigoInterno, codigoInterno: data.codigoInterno,
categoria: data.categoria, categoria: data.categoria,
descricao: data.descricao, descricao: data.descricao,
descricao_mobile: data.descricao_mobile || undefined,
especialidadeId: data.especialidadeId, especialidadeId: data.especialidadeId,
especialidadeNome: esp?.nome || '', especialidadeNome: esp?.nome || '',
valorParticular: parseFloat(data.valorParticular), valorParticular: parseFloat(data.valorParticular),
@@ -644,6 +645,12 @@ export const ProcedimentosView: React.FC = () => {
</div> </div>
</div> </div>
<textarea name="descricao" placeholder="BREVE DESCRIÇÃO" defaultValue={editingProcedimento?.descricao} className="w-full border border-gray-300 rounded-lg p-2 h-20 uppercase-textarea"></textarea> <textarea name="descricao" placeholder="BREVE DESCRIÇÃO" defaultValue={editingProcedimento?.descricao} className="w-full border border-gray-300 rounded-lg p-2 h-20 uppercase-textarea"></textarea>
<div>
<label className="text-xs font-bold text-gray-500 uppercase flex items-center gap-2">
DESCRIÇÃO MOBILE <span className="text-[10px] text-blue-500 font-normal normal-case">(versão curta para celular)</span>
</label>
<input name="descricao_mobile" placeholder="EX: RESTAURAÇÃO COMP." defaultValue={editingProcedimento?.descricao_mobile} className="w-full border border-gray-300 rounded-lg p-2 uppercase-input" />
</div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-xs font-bold text-gray-500 uppercase">ESPECIALIDADE</label> <label className="text-xs font-bold text-gray-500 uppercase">ESPECIALIDADE</label>
+368 -204
View File
@@ -9,6 +9,7 @@ import {
Camera, Camera,
Stethoscope, Stethoscope,
ChevronRight, ChevronRight,
ChevronLeft,
Search, Search,
X X
} from 'lucide-react'; } from 'lucide-react';
@@ -72,17 +73,17 @@ export { useGTOStore };
// --- ODONTOGRAM DATA (4x2 Grid Layout) --- // --- ODONTOGRAM DATA (4x2 Grid Layout) ---
const DENTES_ADULTO = { const DENTES_ADULTO = {
q1: [14, 13, 12, 11, 18, 17, 16, 15], // Top: 14-11, Bottom: 18-15 q1: [14, 13, 12, 11, 18, 17, 16, 15],
q2: [21, 22, 23, 24, 25, 26, 27, 28], // Top: 21-24, Bottom: 25-28 q2: [21, 22, 23, 24, 25, 26, 27, 28],
q4: [44, 43, 42, 41, 48, 47, 46, 45], // Top: 44-41, Bottom: 48-45 q4: [44, 43, 42, 41, 48, 47, 46, 45],
q3: [31, 32, 33, 34, 35, 36, 37, 38], // Top: 31-34, Bottom: 35-38 q3: [31, 32, 33, 34, 35, 36, 37, 38],
extras: { q1: 19, q2: 29, q4: 49, q3: 39 } extras: { q1: 19, q2: 29, q4: 49, q3: 39 }
}; };
const DENTES_CRIANCA = { const DENTES_CRIANCA = {
q1: [null, 53, 52, 51, null, null, 55, 54], q1: [null, 53, 52, 51, null, null, 55, 54],
q2: [61, 62, 63, null, 64, 65, null, null], q2: [61, 62, 63, null, 64, 65, null, null],
q4: [null, 83, 82, 81, null, null, 85, 84], // fix: eram 43-46 (permanentes) q4: [null, 83, 82, 81, null, null, 85, 84],
q3: [71, 72, 73, null, 74, 75, null, null] q3: [71, 72, 73, null, 74, 75, null, null]
}; };
@@ -105,8 +106,14 @@ export const LancarGTO: React.FC = () => {
const [showMixed, setShowMixed] = useState(false); const [showMixed, setShowMixed] = useState(false);
const [selectedDentes, setSelectedDentes] = useState<string[]>([]); const [selectedDentes, setSelectedDentes] = useState<string[]>([]);
const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null); const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null);
const [face, setFace] = useState('');
const [obs, setObs] = useState(''); // Per-tooth face/obs
const [denteInputs, setDenteInputs] = useState<Record<string, { face: string; obs: string }>>({});
// 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 [patientSearchOpen, setPatientSearchOpen] = useState(false);
const [patientSearchTerm, setPatientSearchTerm] = useState(''); const [patientSearchTerm, setPatientSearchTerm] = useState('');
@@ -138,7 +145,6 @@ export const LancarGTO: React.FC = () => {
).slice(0, 30); ).slice(0, 30);
}, [pacientes, patientSearchTerm]); // eslint-disable-line react-hooks/exhaustive-deps }, [pacientes, patientSearchTerm]); // eslint-disable-line react-hooks/exhaustive-deps
// Auto-seleciona paciente quando vem da agenda (pendingSearch)
useEffect(() => { useEffect(() => {
const { pendingSearch } = store; const { pendingSearch } = store;
if (!pendingSearch || pacientes.length === 0) return; if (!pendingSearch || pacientes.length === 0) return;
@@ -158,15 +164,27 @@ export const LancarGTO: React.FC = () => {
store.setPendingSearch(''); store.setPendingSearch('');
}, [pacientes, store.pendingSearch]); // eslint-disable-line react-hooks/exhaustive-deps }, [pacientes, store.pendingSearch]); // eslint-disable-line react-hooks/exhaustive-deps
const resetProcSelection = () => {
setSelectedDentes([]);
setSelectedArco(null);
setDenteInputs({});
setArcoFace('');
setArcoObs('');
};
const toggleDente = (num: string) => { const toggleDente = (num: string) => {
if (selectedProc?.tipo_regiao !== 'DENTE') { if (selectedProc?.tipo_regiao !== 'DENTE') {
toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL."); toast.error("ESTE PROCEDIMENTO NÃO PERMITE SELEÇÃO DE DENTE INDIVIDUAL.");
return; return;
} }
setSelectedArco(null); setSelectedArco(null);
setSelectedDentes(prev => if (selectedDentes.includes(num)) {
prev.includes(num) ? prev.filter(d => d !== num) : [...prev, 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') => { const selectArco = (arco: 'AI' | 'AS') => {
@@ -175,6 +193,7 @@ export const LancarGTO: React.FC = () => {
return; return;
} }
setSelectedDentes([]); setSelectedDentes([]);
setDenteInputs({});
setSelectedArco(arco); setSelectedArco(arco);
}; };
@@ -184,7 +203,6 @@ export const LancarGTO: React.FC = () => {
return; return;
} }
// Validação de Região
if (selectedProc.tipo_regiao === 'DENTE' && selectedDentes.length === 0) { if (selectedProc.tipo_regiao === 'DENTE' && selectedDentes.length === 0) {
toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE AO MENOS UM DENTE."); toast.error("ESTE PROCEDIMENTO EXIGE A SELEÇÃO DE AO MENOS UM DENTE.");
return; return;
@@ -195,35 +213,53 @@ export const LancarGTO: React.FC = () => {
return; return;
} }
// Validação de Face if (selectedProc.tipo_regiao === 'DENTE') {
if (selectedProc.exige_face && (!face || face.trim() === '')) { if (selectedProc.exige_face) {
toast.error("A FACE É OBRIGATÓRIA PARA ESTE PROCEDIMENTO."); const semFace = selectedDentes.filter(d => !denteInputs[d]?.face?.trim());
if (semFace.length > 0) {
toast.error(`FACE OBRIGATÓRIA PARA: ${semFace.join(', ')}`);
return; return;
} }
}
const newItem: GTOItem = { selectedDentes.forEach(dente => {
const inp = denteInputs[dente] || { face: '', obs: '' };
store.addItem({
id: Math.random().toString(36).substring(7), id: Math.random().toString(36).substring(7),
procedimentoId: selectedProc.id, procedimentoId: selectedProc.id,
codigoTUSS: selectedProc.codigo || '000000', codigoTUSS: selectedProc.codigo || '000000',
codigoInterno: selectedProc.codigoInterno || '', codigoInterno: selectedProc.codigoInterno || '',
descricao: selectedProc.nome, descricao: selectedProc.nome,
dente: selectedDentes.join(', '), dente,
arco: selectedArco || undefined, face: inp.face,
face: face,
quantidade: 1, quantidade: 1,
valorUnitario: selectedProc.valorParticular || 0, valorUnitario: selectedProc.valorParticular || 0,
valorTotal: selectedProc.valorParticular || 0, valorTotal: selectedProc.valorParticular || 0,
observacao: obs, observacao: inp.obs || undefined,
}; });
});
store.addItem(newItem); 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."); toast.success("ADICIONADO AO RASCUNHO.");
}
// Reset selection fields resetProcSelection();
setSelectedDentes([]);
setSelectedArco(null);
setFace('');
setObs('');
}; };
const handleFinalize = async () => { const handleFinalize = async () => {
@@ -231,12 +267,12 @@ export const LancarGTO: React.FC = () => {
toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS."); toast.error("PACIENTE, DENTISTA E PELO MENOS 1 ITEM SÃO OBRIGATÓRIOS.");
return; return;
} }
try { try {
toast.success("GTO GERADA COM SUCESSO!"); toast.success("GTO GERADA COM SUCESSO!");
store.clear(); store.clear();
setSelectedEsp(''); setSelectedEsp('');
setSelectedProc(null); setSelectedProc(null);
resetProcSelection();
} catch { } catch {
toast.error("ERRO AO GERAR GTO."); toast.error("ERRO AO GERAR GTO.");
} }
@@ -246,10 +282,13 @@ export const LancarGTO: React.FC = () => {
const toggleMixed = () => { const toggleMixed = () => {
if (showMixed) { if (showMixed) {
// Ao fechar: remove os dentes do tipo expandido da seleção
if (pacienteTipo === 'ADULTO') { 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)); setSelectedDentes(prev => prev.filter(d => parseInt(d) < 50));
} else { } 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)); setSelectedDentes(prev => prev.filter(d => parseInt(d) >= 50));
} }
} }
@@ -285,30 +324,244 @@ export const LancarGTO: React.FC = () => {
); );
}; };
// ─── JSX SECTIONS ───────────────────────────────────────────────
const step1Card = (
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-blue-600 font-black text-base uppercase tracking-wider">
<Users size={20} strokeWidth={3} /> 01. Paciente
</div>
{store.paciente ? (
<div className="flex items-center justify-between bg-blue-50 border-2 border-blue-200 rounded-2xl px-4 py-3">
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-black text-xs flex-shrink-0">
{store.paciente.nome.charAt(0)}
</div>
<span className="font-black text-blue-800 text-sm uppercase truncate">{store.paciente.nome}</span>
</div>
<button
onClick={() => store.setPaciente(null)}
className="text-xs font-bold text-blue-500 hover:text-red-500 transition-colors flex-shrink-0 ml-3 uppercase"
>
ALTERAR
</button>
</div>
) : (
<button
onClick={() => setPatientSearchOpen(true)}
className="w-full p-4 bg-gray-50 border-2 border-dashed border-blue-200 rounded-2xl font-black text-blue-400 text-sm hover:bg-blue-50 hover:border-blue-400 transition-all uppercase flex items-center justify-center gap-2"
>
<Search size={16} /> SELECIONAR BENEFICIÁRIO
</button>
)}
</div>
);
const step2Card = (
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-5">
<div className="flex items-center gap-3 text-orange-500 font-black text-base uppercase tracking-wider">
<Stethoscope size={20} strokeWidth={3} /> 02. Especialidade
</div>
<div className="space-y-3">
<select
value={selectedEsp}
onChange={(e) => {
setSelectedEsp(e.target.value);
setSelectedProc(null);
resetProcSelection();
}}
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"
>
<option value="">ESCOLHA A ÁREA...</option>
{especialidades.map(es => <option key={es.id} value={es.id}>{es.nome}</option>)}
</select>
{selectedEsp && (
<div className="space-y-2">
<select
value={selectedProc?.id || ''}
onChange={(e) => {
const pr = procedimentos.find(p => p.id === e.target.value);
setSelectedProc(pr || null);
resetProcSelection();
}}
className="w-full p-4 bg-white border-2 border-orange-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase"
>
<option value="">BUSCAR PROCEDIMENTO</option>
{filteredProcedimentos.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
</select>
{selectedProc && (
<div className="bg-orange-50 p-3 rounded-xl border border-orange-100 animate-in fade-in slide-in-from-top-1 duration-300">
<p className="text-[9px] font-black text-orange-600 uppercase mb-1">REGRAS DE SELEÇÃO</p>
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-orange-800 flex items-center gap-1">
<ChevronRight size={10} />
{selectedProc.tipo_regiao === 'DENTE' ? 'EXIGE DENTE(S)' : selectedProc.tipo_regiao === 'ARCO' ? 'EXIGE ARCO (AI/AS)' : 'PROCEDIMENTO GERAL'}
</span>
{selectedProc.exige_face && (
<span className="text-[10px] font-bold text-red-600 flex items-center gap-1">
<ChevronRight size={10} /> EXIGE FACE CLÍNICA
</span>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
const step3Card = (
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-[#2d6a4f] font-black text-base uppercase tracking-wider">
<Check size={20} strokeWidth={3} className="bg-green-100 p-1 rounded-full text-green-700" /> 03. Dentista Executor
</div>
<select
value={store.dentista?.id || ''}
onChange={(e) => {
const d = dentistas.find(d => d.id === e.target.value);
store.setDentista(d || null);
}}
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-green-500 outline-none transition-all uppercase"
>
<option value="">SELECIONE O PROFISSIONAL</option>
{dentistas.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
</select>
</div>
);
// Per-tooth inputs (DENTE procedure) or unified (ARCO/GERAL)
const isPerTooth = selectedProc?.tipo_regiao === 'DENTE' && selectedDentes.length > 0;
const step4Card = (
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4">
<div className="flex items-center justify-between">
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">
04. {isPerTooth ? `Face / Obs por Dente (${selectedDentes.length})` : 'Face & Observações'}
</p>
{selectedProc?.exige_face && (
<span className="text-red-500 text-[9px] font-black animate-pulse">* OBRIGATÓRIO</span>
)}
</div>
{isPerTooth ? (
<div className="space-y-3 max-h-64 overflow-y-auto pr-1">
{selectedDentes.map(dente => {
const isDeciduo = parseInt(dente) >= 50;
const inp = denteInputs[dente] || { face: '', obs: '' };
return ( return (
<div className="space-y-6"> <div key={dente} className="bg-gray-50 rounded-2xl p-3 border border-gray-100 space-y-2">
<PageHeader <div className="flex items-center gap-2">
title="LANÇAR GTO" <span className={`w-8 h-8 rounded-lg flex items-center justify-center text-[11px] font-black flex-shrink-0
description="MONTGEM DINÂMICA DE GUIA ODONTOLÓGICA (GTO)." ${isDeciduo ? 'bg-amber-100 text-amber-700 border border-amber-200' : 'bg-blue-100 text-blue-700 border border-blue-200'}`}>
{dente}
</span>
<span className="text-[10px] font-bold text-gray-500 uppercase truncate flex-1">
{selectedProc?.descricao_mobile || selectedProc?.nome}
</span>
</div>
<div className="grid grid-cols-2 gap-2">
<input
type="text"
value={inp.face}
onChange={e => setDenteInputs(prev => ({
...prev,
[dente]: { ...(prev[dente] || {}), face: e.target.value.toUpperCase() }
}))}
placeholder={selectedProc?.exige_face ? 'FACE *' : 'O,M,D,V,L,P'}
className={`p-2 border-2 rounded-xl text-[11px] font-black outline-none transition-all
${selectedProc?.exige_face && !inp.face
? 'border-red-200 bg-red-50 focus:border-red-400'
: 'border-gray-200 bg-white focus:border-amber-400'}`}
/> />
<input
type="text"
value={inp.obs}
onChange={e => setDenteInputs(prev => ({
...prev,
[dente]: { ...(prev[dente] || {}), obs: e.target.value }
}))}
placeholder="OBS"
className="p-2 border-2 border-gray-200 bg-white rounded-xl text-[11px] outline-none focus:border-amber-400 transition-all"
/>
</div>
</div>
);
})}
</div>
) : (
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2">
Face (O, M, D, V, L, P)
</label>
<input
type="text"
value={arcoFace}
onChange={e => 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"}
/>
</div>
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 italic">Observações Clínicas</label>
<div className="relative">
<textarea
value={arcoObs}
onChange={e => setArcoObs(e.target.value)}
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl text-[11px] h-20 focus:border-amber-400 outline-none transition-all"
/>
<PlusCircle size={24} className="absolute bottom-4 right-4 text-green-500 opacity-50" />
</div>
</div>
</div>
)}
</div>
);
<div className="grid grid-cols-12 gap-6 items-start"> const actionsDesktop = (
<div className="grid grid-cols-4 gap-4 p-2">
<button
onClick={handleAddItem}
className="col-span-3 bg-[#2d6a4f] text-white py-5 rounded-3xl font-black text-lg uppercase shadow-2xl shadow-green-200 hover:bg-[#1b4332] active:scale-95 transition-all flex items-center justify-center gap-3 group"
>
ADICIONAR ITEM <ChevronRight className="group-hover:translate-x-1 transition-transform" />
</button>
<button
onClick={handleFinalize}
className="col-span-1 bg-gray-900 text-white py-5 rounded-3xl flex items-center justify-center shadow-xl hover:bg-black active:scale-95 transition-all"
title="FINALIZAR GTO"
>
<Check size={32} strokeWidth={4} />
</button>
</div>
);
{/* LEFT PANEL: ODONTOGRAM & ITEMS */} const actionsMobile = (
<div className="col-span-12 lg:col-span-8 space-y-6"> <button
onClick={handleAddItem}
className="w-full bg-[#2d6a4f] text-white py-5 rounded-3xl font-black text-lg uppercase shadow-2xl shadow-green-200 hover:bg-[#1b4332] active:scale-95 transition-all flex items-center justify-center gap-3 group"
>
ADICIONAR ITEM <ChevronRight className="group-hover:translate-x-1 transition-transform" />
</button>
);
{/* ODONTOGRAM BOX */} const odontogramCard = (
<div className="bg-white rounded-3xl border border-gray-100 shadow-xl p-3 sm:p-8 space-y-4 sm:space-y-6"> <div className="bg-white rounded-3xl border border-gray-100 shadow-xl p-3 sm:p-8 space-y-4 sm:space-y-6">
<div className="flex justify-center mb-2 sm:mb-4"> <div className="flex justify-center mb-2 sm:mb-4">
<div className="inline-flex bg-gray-100 p-1 rounded-xl shadow-inner"> <div className="inline-flex bg-gray-100 p-1 rounded-xl shadow-inner">
<button <button
onClick={() => { setPacienteTipo('ADULTO'); setSelectedDentes([]); setShowMixed(false); }} onClick={() => { setPacienteTipo('ADULTO'); setSelectedDentes([]); setDenteInputs({}); setShowMixed(false); }}
className={`px-5 sm:px-8 py-2 rounded-lg text-xs font-black transition-all uppercase ${pacienteTipo === 'ADULTO' ? 'bg-blue-600 text-white shadow-md scale-105' : 'text-gray-500 hover:text-gray-800'}`} className={`px-5 sm:px-8 py-2 rounded-lg text-xs font-black transition-all uppercase ${pacienteTipo === 'ADULTO' ? 'bg-blue-600 text-white shadow-md scale-105' : 'text-gray-500 hover:text-gray-800'}`}
> >
Adulto Adulto
</button> </button>
<button <button
onClick={() => { setPacienteTipo('CRIANCA'); setSelectedDentes([]); setShowMixed(false); }} onClick={() => { setPacienteTipo('CRIANCA'); setSelectedDentes([]); setDenteInputs({}); setShowMixed(false); }}
className={`px-5 sm:px-8 py-2 rounded-lg text-xs font-black transition-all uppercase ${pacienteTipo === 'CRIANCA' ? 'bg-blue-600 text-white shadow-md scale-105' : 'text-gray-500 hover:text-gray-800'}`} className={`px-5 sm:px-8 py-2 rounded-lg text-xs font-black transition-all uppercase ${pacienteTipo === 'CRIANCA' ? 'bg-blue-600 text-white shadow-md scale-105' : 'text-gray-500 hover:text-gray-800'}`}
> >
Criança Criança
@@ -316,13 +569,11 @@ export const LancarGTO: React.FC = () => {
</div> </div>
</div> </div>
{/* Dynamic Odontogram Grid */}
<div className="relative border-t border-b border-gray-50 py-6 sm:py-10"> <div className="relative border-t border-b border-gray-50 py-6 sm:py-10">
<div className="absolute left-1/2 top-4 bottom-4 w-[2px] bg-gray-200 -translate-x-1/2"></div> <div className="absolute left-1/2 top-4 bottom-4 w-[2px] bg-gray-200 -translate-x-1/2"></div>
<div className="absolute top-1/2 left-2 right-2 h-[2px] bg-gray-200 -translate-y-1/2"></div> <div className="absolute top-1/2 left-2 right-2 h-[2px] bg-gray-200 -translate-y-1/2"></div>
<div className="grid grid-cols-2 gap-x-2 sm:gap-x-12 gap-y-6 sm:gap-y-16"> <div className="grid grid-cols-2 gap-x-2 sm:gap-x-12 gap-y-6 sm:gap-y-16">
{/* Q1 & Q2 (SUPERIOR) */}
<div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2"> <div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2">
{pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)} {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
<div className="grid grid-cols-4 gap-1 sm:gap-2"> <div className="grid grid-cols-4 gap-1 sm:gap-2">
@@ -335,8 +586,6 @@ export const LancarGTO: React.FC = () => {
</div> </div>
{pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)} {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
</div> </div>
{/* Q4 & Q3 (INFERIOR) */}
<div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2"> <div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2">
{pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)} {pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
<div className="grid grid-cols-4 gap-1 sm:gap-2"> <div className="grid grid-cols-4 gap-1 sm:gap-2">
@@ -352,7 +601,6 @@ export const LancarGTO: React.FC = () => {
</div> </div>
</div> </div>
{/* SEÇÃO MISTO — expande abaixo do grid principal */}
{showMixed && ( {showMixed && (
<div className="border-t-2 border-dashed border-amber-200 pt-4 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200"> <div className="border-t-2 border-dashed border-amber-200 pt-4 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
<p className="text-[9px] font-black text-amber-500 uppercase tracking-widest text-center"> <p className="text-[9px] font-black text-amber-500 uppercase tracking-widest text-center">
@@ -362,7 +610,6 @@ export const LancarGTO: React.FC = () => {
<div className="absolute left-1/2 top-2 bottom-2 w-[2px] bg-amber-100 -translate-x-1/2"></div> <div className="absolute left-1/2 top-2 bottom-2 w-[2px] bg-amber-100 -translate-x-1/2"></div>
<div className="absolute top-1/2 left-2 right-2 h-[2px] bg-amber-100 -translate-y-1/2"></div> <div className="absolute top-1/2 left-2 right-2 h-[2px] bg-amber-100 -translate-y-1/2"></div>
<div className="grid grid-cols-2 gap-x-2 sm:gap-x-12 gap-y-6 sm:gap-y-16"> <div className="grid grid-cols-2 gap-x-2 sm:gap-x-12 gap-y-6 sm:gap-y-16">
{/* Q1 & Q2 MISTO */}
<div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2"> <div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2">
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q1)} {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
<div className="grid grid-cols-4 gap-1 sm:gap-2"> <div className="grid grid-cols-4 gap-1 sm:gap-2">
@@ -375,7 +622,6 @@ export const LancarGTO: React.FC = () => {
</div> </div>
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q2)} {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
</div> </div>
{/* Q4 & Q3 MISTO */}
<div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2"> <div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2">
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q4)} {pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
<div className="grid grid-cols-4 gap-1 sm:gap-2"> <div className="grid grid-cols-4 gap-1 sm:gap-2">
@@ -393,7 +639,6 @@ export const LancarGTO: React.FC = () => {
</div> </div>
)} )}
{/* BOTÃO EXPANSOR — dentição mista */}
<button <button
onClick={toggleMixed} onClick={toggleMixed}
className={`w-full py-2.5 rounded-2xl border-2 font-black text-[11px] uppercase transition-all flex items-center justify-center gap-2 className={`w-full py-2.5 rounded-2xl border-2 font-black text-[11px] uppercase transition-all flex items-center justify-center gap-2
@@ -430,14 +675,14 @@ export const LancarGTO: React.FC = () => {
</button> </button>
</div> </div>
</div> </div>
);
{/* RASCUNHO LIST */} const rascunhoCard = (
<div className="bg-white rounded-3xl border border-gray-100 overflow-hidden shadow-xl"> <div className="bg-white rounded-3xl border border-gray-100 overflow-hidden shadow-xl">
<div className="bg-[#d49a4a] p-4 text-white font-black text-sm uppercase flex justify-between items-center tracking-widest"> <div className="bg-[#d49a4a] p-4 text-white font-black text-sm uppercase flex justify-between items-center tracking-widest">
<span>PROCEDIMENTOS DO RASCUNHO</span> <span>PROCEDIMENTOS DO RASCUNHO</span>
<div className="bg-white/20 px-3 py-1 rounded-full text-[10px]">{store.items.length}/20 ITENS</div> <div className="bg-white/20 px-3 py-1 rounded-full text-[10px]">{store.items.length}/20 ITENS</div>
</div> </div>
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
{store.items.length === 0 && ( {store.items.length === 0 && (
<div className="py-16 text-center text-gray-300 font-bold uppercase flex flex-col items-center gap-4"> <div className="py-16 text-center text-gray-300 font-bold uppercase flex flex-col items-center gap-4">
@@ -447,7 +692,6 @@ export const LancarGTO: React.FC = () => {
NÃO ITENS ADICIONADOS NÃO ITENS ADICIONADOS
</div> </div>
)} )}
{store.items.map((item) => ( {store.items.map((item) => (
<div key={item.id} className="border border-gray-100 rounded-2xl p-5 bg-gray-50/30 hover:bg-white transition-all shadow-sm group"> <div key={item.id} className="border border-gray-100 rounded-2xl p-5 bg-gray-50/30 hover:bg-white transition-all shadow-sm group">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
@@ -464,14 +708,12 @@ export const LancarGTO: React.FC = () => {
<Trash2 size={20} /> <Trash2 size={20} />
</button> </button>
</div> </div>
<div className="grid grid-cols-4 gap-6 text-[11px] font-black text-gray-500 uppercase tracking-wider mb-4 border-t border-gray-100 pt-4"> <div className="grid grid-cols-4 gap-6 text-[11px] font-black text-gray-500 uppercase tracking-wider mb-4 border-t border-gray-100 pt-4">
<div>DENTE: <span className="text-blue-600 ml-1">{item.dente || '--'}</span></div> <div>DENTE: <span className="text-blue-600 ml-1">{item.dente || '--'}</span></div>
<div>ARCO: <span className="text-amber-600 ml-1">{item.arco || '--'}</span></div> <div>ARCO: <span className="text-amber-600 ml-1">{item.arco || '--'}</span></div>
<div>FACE: <span className="text-gray-900 ml-1">{item.face || '--'}</span></div> <div>FACE: <span className="text-gray-900 ml-1">{item.face || '--'}</span></div>
<div>VALOR: <span className="text-green-600 ml-1 text-xs">R$ {item.valorTotal.toFixed(2)}</span></div> <div>VALOR: <span className="text-green-600 ml-1 text-xs">R$ {item.valorTotal.toFixed(2)}</span></div>
</div> </div>
{item.observacao && ( {item.observacao && (
<div className="text-[11px] text-gray-500 bg-white p-3 border border-gray-100 rounded-xl leading-relaxed"> <div className="text-[11px] text-gray-500 bg-white p-3 border border-gray-100 rounded-xl leading-relaxed">
<span className="font-black text-gray-400 uppercase mr-2 opacity-50">OBS:</span> {item.observacao} <span className="font-black text-gray-400 uppercase mr-2 opacity-50">OBS:</span> {item.observacao}
@@ -479,7 +721,6 @@ export const LancarGTO: React.FC = () => {
)} )}
</div> </div>
))} ))}
<div className="pt-6 border-t-2 border-dotted border-gray-200 flex justify-between items-center"> <div className="pt-6 border-t-2 border-dotted border-gray-200 flex justify-between items-center">
<button className="flex items-center gap-2 text-blue-600 font-black text-[11px] uppercase hover:bg-blue-50 px-4 py-2 rounded-xl transition-all"> <button className="flex items-center gap-2 text-blue-600 font-black text-[11px] uppercase hover:bg-blue-50 px-4 py-2 rounded-xl transition-all">
<Camera size={16} /> ANEXAR DOCUMENTOS <Camera size={16} /> ANEXAR DOCUMENTOS
@@ -491,169 +732,92 @@ export const LancarGTO: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
);
// ─── RENDER ─────────────────────────────────────────────────────
return (
<div className="space-y-6 pb-20 lg:pb-0">
<PageHeader
title="LANÇAR GTO"
description="MONTAGEM DINÂMICA DE GUIA ODONTOLÓGICA (GTO)."
/>
{/* ─── DESKTOP LAYOUT ─────────────────────────────── */}
<div className="hidden lg:grid grid-cols-12 gap-6 items-start">
<div className="col-span-8 space-y-6">
{odontogramCard}
{rascunhoCard}
</div>
<div className="col-span-4 space-y-4 sticky top-6">
{step1Card}
{step2Card}
{step3Card}
{step4Card}
{actionsDesktop}
</div>
</div> </div>
{/* RIGHT PANEL: WORKFLOW SELECTORS */} {/* ─── MOBILE LAYOUT (3 páginas) ──────────────────── */}
<div className="col-span-12 lg:col-span-4 space-y-4 sticky top-6"> <div className="lg:hidden">
{mobilePage === 1 && (
<div className="space-y-4">
{step1Card}
{step2Card}
{step3Card}
</div>
)}
{mobilePage === 2 && odontogramCard}
{mobilePage === 3 && (
<div className="space-y-4">
{step4Card}
{actionsMobile}
{rascunhoCard}
</div>
)}
</div>
{/* STEP 1: PACIENTE */} {/* ─── MOBILE FOOTER NAV ──────────────────────────── */}
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4"> <div className="fixed bottom-0 left-0 right-0 z-40 lg:hidden bg-white/95 backdrop-blur-sm border-t border-gray-200 shadow-xl px-4 py-3 flex items-center justify-between gap-2">
<div className="flex items-center gap-3 text-blue-600 font-black text-base uppercase tracking-wider"> {mobilePage > 1 ? (
<Users size={20} strokeWidth={3} /> 01. Paciente
</div>
{store.paciente ? (
<div className="flex items-center justify-between bg-blue-50 border-2 border-blue-200 rounded-2xl px-4 py-3">
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center text-white font-black text-xs flex-shrink-0">
{store.paciente.nome.charAt(0)}
</div>
<span className="font-black text-blue-800 text-sm uppercase truncate">{store.paciente.nome}</span>
</div>
<button <button
onClick={() => store.setPaciente(null)} onClick={() => setMobilePage(p => (p - 1) as 1 | 2 | 3)}
className="text-xs font-bold text-blue-500 hover:text-red-500 transition-colors flex-shrink-0 ml-3 uppercase" className="flex items-center gap-1.5 px-4 py-2.5 rounded-2xl border-2 border-gray-200 font-black text-xs text-gray-500 hover:bg-gray-50 transition-all uppercase"
> >
ALTERAR <ChevronLeft size={14} /> VOLTAR
</button> </button>
) : (
<div className="w-24" />
)}
<div className="flex items-center gap-2">
{([1, 2, 3] as const).map(p => (
<button
key={p}
onClick={() => setMobilePage(p)}
className={`transition-all rounded-full ${mobilePage === p ? 'w-6 h-2.5 bg-blue-600' : 'w-2.5 h-2.5 bg-gray-200 hover:bg-gray-400'}`}
/>
))}
</div> </div>
{mobilePage < 3 ? (
<button
onClick={() => setMobilePage(p => (p + 1) as 1 | 2 | 3)}
className="flex items-center gap-1.5 px-4 py-2.5 rounded-2xl bg-blue-600 text-white font-black text-xs hover:bg-blue-700 active:scale-95 transition-all uppercase shadow-md"
>
PRÓXIMO <ChevronRight size={14} />
</button>
) : ( ) : (
<button
onClick={() => setPatientSearchOpen(true)}
className="w-full p-4 bg-gray-50 border-2 border-dashed border-blue-200 rounded-2xl font-black text-blue-400 text-sm hover:bg-blue-50 hover:border-blue-400 transition-all uppercase flex items-center justify-center gap-2"
>
<Search size={16} /> SELECIONAR BENEFICIÁRIO
</button>
)}
</div>
{/* STEP 2: ESPECIALIDADE & PROCEDIMENTO */}
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-5">
<div className="flex items-center gap-3 text-orange-500 font-black text-base uppercase tracking-wider">
<Stethoscope size={20} strokeWidth={3} /> 02. Especialidade
</div>
<div className="space-y-3">
<select
value={selectedEsp}
onChange={(e) => {
setSelectedEsp(e.target.value);
setSelectedProc(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"
>
<option value="">ESCOLHA A ÁREA...</option>
{especialidades.map(es => <option key={es.id} value={es.id}>{es.nome}</option>)}
</select>
{selectedEsp && (
<div className="space-y-2">
<select
value={selectedProc?.id || ''}
onChange={(e) => {
const pr = procedimentos.find(p => p.id === e.target.value);
setSelectedProc(pr || null);
setSelectedDentes([]);
setSelectedArco(null);
}}
className="w-full p-4 bg-white border-2 border-orange-100 rounded-2xl font-black text-gray-800 text-sm shadow-inner uppercase"
>
<option value="">BUSCAR PROCEDIMENTO</option>
{filteredProcedimentos.map(p => <option key={p.id} value={p.id}>{p.nome}</option>)}
</select>
{selectedProc && (
<div className="bg-orange-50 p-3 rounded-xl border border-orange-100 animate-in fade-in slide-in-from-top-1 duration-300">
<p className="text-[9px] font-black text-orange-600 uppercase mb-1">REGRAS DE SELEÇÃO</p>
<div className="flex flex-col gap-1">
<span className="text-[10px] font-bold text-orange-800 flex items-center gap-1">
<ChevronRight size={10} />
{selectedProc.tipo_regiao === 'DENTE' ? 'EXIGE DENTE(S)' : selectedProc.tipo_regiao === 'ARCO' ? 'EXIGE ARCO (AI/AS)' : 'PROCEDIMENTO GERAL'}
</span>
{selectedProc.exige_face && (
<span className="text-[10px] font-bold text-red-600 flex items-center gap-1">
<ChevronRight size={10} /> EXIGE FACE CLINICA
</span>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
{/* STEP 3: DENTISTA */}
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4">
<div className="flex items-center gap-3 text-[#2d6a4f] font-black text-base uppercase tracking-wider">
<Check size={20} strokeWidth={3} className="bg-green-100 p-1 rounded-full text-green-700" /> 03. Dentista Executor
</div>
<select
value={store.dentista?.id || ''}
onChange={(e) => {
const d = dentistas.find(d => d.id === e.target.value);
store.setDentista(d || null);
}}
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl font-black text-gray-800 text-sm focus:border-green-500 outline-none transition-all uppercase"
>
<option value="">SELECIONE O PROFISSIONAL</option>
{dentistas.map(d => <option key={d.id} value={d.id}>{d.nome}</option>)}
</select>
</div>
{/* STEP 4: DETALHES (FACE/OBS) */}
<div className="bg-white p-6 rounded-3xl border border-gray-100 shadow-xl space-y-4">
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 flex justify-between">
<span>Face (O, M, D, V, L, P)</span>
{selectedProc?.exige_face && <span className="text-red-500 font-black animate-pulse">* OBRIGATÓRIO</span>}
</label>
<input
type="text"
value={face}
onChange={e => 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"}
/>
</div>
<div>
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 italic">Observações Clínicas</label>
<div className="relative">
<textarea
value={obs}
onChange={e => setObs(e.target.value)}
className="w-full p-4 bg-gray-50 border-2 border-gray-100 rounded-2xl text-[11px] h-20 focus:border-amber-400 outline-none transition-all"
/>
<PlusCircle size={24} className="absolute bottom-4 right-4 text-green-500 opacity-50" />
</div>
</div>
</div>
</div>
{/* ACTIONS */}
<div className="grid grid-cols-4 gap-4 p-2">
<button
onClick={handleAddItem}
className="col-span-3 bg-[#2d6a4f] text-white py-5 rounded-3xl font-black text-lg uppercase shadow-2xl shadow-green-200 hover:bg-[#1b4332] active:scale-95 transition-all flex items-center justify-center gap-3 group"
>
ADICIONAR ITEM <ChevronRight className="group-hover:translate-x-1 transition-transform" />
</button>
<button <button
onClick={handleFinalize} onClick={handleFinalize}
className="col-span-1 bg-gray-900 text-white py-5 rounded-3xl flex items-center justify-center shadow-xl hover:bg-black active:scale-95 transition-all" className="flex items-center gap-1.5 px-4 py-2.5 rounded-2xl bg-gray-900 text-white font-black text-xs hover:bg-black active:scale-95 transition-all uppercase shadow-md"
title="FINALIZAR GTO"
> >
<Check size={32} strokeWidth={4} /> FINALIZAR <Check size={14} />
</button> </button>
)}
</div> </div>
</div> {/* ─── PATIENT SEARCH MODAL ───────────────────────── */}
</div>
{/* MODAL DE BUSCA DE PACIENTE */}
{patientSearchOpen && ( {patientSearchOpen && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-4"> <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm p-4">
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md flex flex-col overflow-hidden animate-in fade-in zoom-in-50 duration-200"> <div className="bg-white rounded-3xl shadow-2xl w-full max-w-md flex flex-col overflow-hidden animate-in fade-in zoom-in-50 duration-200">