feat(odontograma): dentição mista + fix mobile + fix dentes decíduos Q4

- Botão expansor por modo: adulto mostra "+ INCLUIR DENTES DECÍDUOS",
  criança mostra "+ INCLUIR DENTES PERMANENTES" (Opção B)
- Seção expandida exibe odontograma complementar com separadores âmbar
  e cor diferenciada: azul=permanente, âmbar=decíduo
- Fechar expansão limpa automaticamente os dentes do tipo removido
- Fix clínico: DENTES_CRIANCA.q4 corrigido de 41-46 (permanentes)
  para 81-85 (decíduos inferiores direitos)
- Mobile: p-3 sm:p-8, gap-x-2 sm:gap-x-12, gap-1 sm:gap-2,
  w-7 h-7 sm:w-9 sm:h-9, text-[9px] sm:text-[11px]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-05-15 04:00:55 +02:00
parent d0f7914c58
commit 4ff45e2c16
+95 -24
View File
@@ -80,10 +80,9 @@ const DENTES_ADULTO = {
};
const DENTES_CRIANCA = {
// Balanced 4x2 distribution with nulls for spacing (matching image layout)
q1: [null, 53, 52, 51, null, null, 55, 54],
q2: [61, 62, 63, null, 64, 65, null, null],
q4: [null, 43, 42, 41, null, null, 46, 45],
q4: [null, 83, 82, 81, null, null, 85, 84], // fix: eram 43-46 (permanentes)
q3: [71, 72, 73, null, 74, 75, null, null]
};
@@ -103,6 +102,7 @@ export const LancarGTO: React.FC = () => {
const [selectedEsp, setSelectedEsp] = useState<string>('');
const [selectedProc, setSelectedProc] = useState<Procedimento | null>(null);
const [pacienteTipo, setPacienteTipo] = useState<'ADULTO' | 'CRIANCA'>('ADULTO');
const [showMixed, setShowMixed] = useState(false);
const [selectedDentes, setSelectedDentes] = useState<string[]>([]);
const [selectedArco, setSelectedArco] = useState<'AI' | 'AS' | null>(null);
const [face, setFace] = useState('');
@@ -244,9 +244,22 @@ export const LancarGTO: React.FC = () => {
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 <div key={`empty-${keyIdx}`} className="w-9 h-9" />;
if (num === null) return <div key={`empty-${keyIdx}`} className="w-7 h-7 sm:w-9 sm:h-9" />;
const sStr = num.toString();
const isDeciduo = parseInt(sStr) >= 50;
const isSelected = selectedDentes.includes(sStr);
const isDisabled = !!selectedProc && selectedProc.tipo_regiao !== 'DENTE';
@@ -255,12 +268,16 @@ export const LancarGTO: React.FC = () => {
key={sStr}
onClick={() => toggleDente(sStr)}
disabled={isDisabled}
className={`w-9 h-9 rounded-lg border-2 text-[11px] font-black transition-all shadow-sm flex items-center justify-center
className={`w-7 h-7 sm:w-9 sm:h-9 rounded-lg border-2 text-[9px] sm:text-[11px] font-black transition-all shadow-sm flex items-center justify-center
${isSelected
? 'bg-blue-600 border-blue-700 text-white scale-110 z-10'
? isDeciduo
? 'bg-amber-500 border-amber-600 text-white scale-110 z-10'
: 'bg-blue-600 border-blue-700 text-white scale-110 z-10'
: isDisabled
? 'bg-gray-50 border-gray-100 text-gray-300 cursor-not-allowed opacity-50'
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-400 hover:bg-blue-50'}
: isDeciduo
? 'bg-white border-amber-200 text-amber-700 hover:border-amber-400 hover:bg-amber-50'
: 'bg-white border-gray-200 text-gray-700 hover:border-blue-400 hover:bg-blue-50'}
`}
>
{sStr}
@@ -281,18 +298,18 @@ export const LancarGTO: React.FC = () => {
<div className="col-span-12 lg:col-span-8 space-y-6">
{/* ODONTOGRAM BOX */}
<div className="bg-white rounded-3xl border border-gray-100 shadow-xl p-8 space-y-6">
<div className="flex justify-center mb-4">
<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="inline-flex bg-gray-100 p-1 rounded-xl shadow-inner">
<button
onClick={() => { setPacienteTipo('ADULTO'); setSelectedDentes([]); }}
className={`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'}`}
onClick={() => { setPacienteTipo('ADULTO'); setSelectedDentes([]); 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'}`}
>
Adulto
</button>
<button
onClick={() => { setPacienteTipo('CRIANCA'); setSelectedDentes([]); }}
className={`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'}`}
onClick={() => { setPacienteTipo('CRIANCA'); setSelectedDentes([]); 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'}`}
>
Criança
</button>
@@ -300,35 +317,34 @@ export const LancarGTO: React.FC = () => {
</div>
{/* Dynamic Odontogram Grid */}
<div className="relative border-t border-b border-gray-50 py-10">
{/* Visual Separators */}
<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 top-1/2 left-4 right-4 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-12 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-2 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)}
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q1 : DENTES_CRIANCA.q1).map((d, i) => renderDenteBtn(d, i))}
</div>
</div>
<div className="flex justify-start items-center gap-2 pl-2">
<div className="grid grid-cols-4 gap-2">
<div className="flex justify-start items-center gap-1 sm:gap-2 pl-1 sm:pl-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q2 : DENTES_CRIANCA.q2).map((d, i) => renderDenteBtn(d, i))}
</div>
{pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
</div>
{/* Q4 & Q3 (INFERIOR) */}
<div className="flex justify-end items-center gap-2 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)}
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q4 : DENTES_CRIANCA.q4).map((d, i) => renderDenteBtn(d, i))}
</div>
</div>
<div className="flex justify-start items-center gap-2 pl-2">
<div className="grid grid-cols-4 gap-2">
<div className="flex justify-start items-center gap-1 sm:gap-2 pl-1 sm:pl-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_ADULTO.q3 : DENTES_CRIANCA.q3).map((d, i) => renderDenteBtn(d, i))}
</div>
{pacienteTipo === 'ADULTO' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
@@ -336,6 +352,61 @@ export const LancarGTO: React.FC = () => {
</div>
</div>
{/* SEÇÃO MISTO — expande abaixo do grid principal */}
{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">
<p className="text-[9px] font-black text-amber-500 uppercase tracking-widest text-center">
{pacienteTipo === 'ADULTO' ? '— DENTES DECÍDUOS —' : '— DENTES PERMANENTES —'}
</p>
<div className="relative py-4 sm:py-6">
<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="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">
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q1)}
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q1 : DENTES_ADULTO.q1).map((d, i) => renderDenteBtn(d, i))}
</div>
</div>
<div className="flex justify-start items-center gap-1 sm:gap-2 pl-1 sm:pl-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q2 : DENTES_ADULTO.q2).map((d, i) => renderDenteBtn(d, i))}
</div>
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q2)}
</div>
{/* Q4 & Q3 MISTO */}
<div className="flex justify-end items-center gap-1 sm:gap-2 pr-1 sm:pr-2">
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q4)}
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q4 : DENTES_ADULTO.q4).map((d, i) => renderDenteBtn(d, i))}
</div>
</div>
<div className="flex justify-start items-center gap-1 sm:gap-2 pl-1 sm:pl-2">
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{(pacienteTipo === 'ADULTO' ? DENTES_CRIANCA.q3 : DENTES_ADULTO.q3).map((d, i) => renderDenteBtn(d, i))}
</div>
{pacienteTipo === 'CRIANCA' && renderDenteBtn(DENTES_ADULTO.extras.q3)}
</div>
</div>
</div>
</div>
)}
{/* BOTÃO EXPANSOR — dentição mista */}
<button
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
${showMixed
? 'border-amber-300 text-amber-600 bg-amber-50 hover:bg-amber-100'
: 'border-dashed border-amber-200 text-amber-400 hover:border-amber-400 hover:bg-amber-50'}`}
>
{showMixed
? <><X size={13} /> {pacienteTipo === 'ADULTO' ? 'REMOVER DECÍDUOS' : 'REMOVER PERMANENTES'}</>
: <><PlusCircle size={13} /> {pacienteTipo === 'ADULTO' ? '+ INCLUIR DENTES DECÍDUOS' : '+ INCLUIR DENTES PERMANENTES'}</>
}
</button>
<div className="flex justify-center gap-4 pt-4">
<button
onClick={() => selectArco('AI')}