From 773e8d4bf2685cb16fc82702d64b99699831cdb2 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Mon, 6 Jul 2026 04:44:38 +0200 Subject: [PATCH] =?UTF-8?q?fix(agenda):=20fuso=20autom=C3=A1tico=20(Americ?= =?UTF-8?q?a/Sao=5FPaulo)=20+=20dentista=20configura=20seus=20hor=C3=A1rio?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TZ=America/Sao_Paulo no backend (compose): a agenda deixa de rodar em UTC — horários/slots passam a ser Brasília automaticamente (corrige deslocamento de 3h). - Acesso do DENTISTA: novo botão "Meus Horários" na agenda do dentista, abrindo a config direto nos horários/folgas DELE (HorariosConfig com initialTab/soDentista, restrito ao próprio registro). Antes só admin/donoclinica alcançavam a config. - Config da clínica: gate passa a incluir donoconsultorio (dono de consultório). Obs.: AgendaView.tsx também traz trabalho acumulado da agenda que estava no working tree. Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 1 + frontend/components/HorariosConfig.tsx | 15 ++-- frontend/views/AgendaView.tsx | 95 +++++++++++++++++--------- 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ed83493..969aa52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,6 +35,7 @@ services: - "./backend/.env" environment: - BAILEYS_ENGINE=infinite + - TZ=America/Sao_Paulo - PORT=8018 - NODE_ENV=production # Senha do banco vem do ambiente (.env raiz em DEV; provisionado fora do git em PROD). diff --git a/frontend/components/HorariosConfig.tsx b/frontend/components/HorariosConfig.tsx index ec59eca..198b2cc 100644 --- a/frontend/components/HorariosConfig.tsx +++ b/frontend/components/HorariosConfig.tsx @@ -62,8 +62,9 @@ const GradeSemanal: React.FC<{ faixas: Faixa[]; onChange: (f: Faixa[]) => void; ); }; -export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string }> = ({ isOpen, onClose, clinicaId }) => { - const [aba, setAba] = useState<'clinica' | 'dentistas'>('clinica'); +export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; clinicaId?: string; initialTab?: 'clinica' | 'dentistas'; soDentista?: boolean }> = ({ isOpen, onClose, clinicaId, initialTab, soDentista }) => { + const [aba, setAba] = useState<'clinica' | 'dentistas'>(initialTab || 'clinica'); + useEffect(() => { if (isOpen && initialTab) setAba(initialTab); }, [isOpen, initialTab]); const [erro, setErro] = useState(null); const [ok, setOk] = useState(null); const [saving, setSaving] = useState(false); @@ -96,11 +97,13 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl if (!clinicaId) return; try { const d = await req(`/clinica/${clinicaId}/dentistas`); - setDentistas(d.dentistas || []); - const first = (d.dentistas || []).find((x: Dentista) => x.eh_voce) || (d.dentistas || [])[0]; + let list: Dentista[] = d.dentistas || []; + if (soDentista) { const meus = list.filter((x) => x.eh_voce); if (meus.length) list = meus; } // dentista só vê o próprio + setDentistas(list); + const first = list.find((x: Dentista) => x.eh_voce) || list[0]; if (first) setDentSel(first.id); } catch (e: any) { flash(setErro, e.message); } - }, [clinicaId]); + }, [clinicaId, soDentista]); useEffect(() => { if (isOpen) { carregarClinica(); carregarDentistas(); } }, [isOpen, carregarClinica, carregarDentistas]); const carregarFolgas = useCallback((id: string) => { @@ -162,7 +165,7 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl {/* Tabs */}
- {(['clinica', 'dentistas'] as const).map((t) => ( + {(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas'] as const)).map((t) => ( + {/* Título + recolher menu (desktop, ao lado). No mobile o AGENDAR e as + ações ficam empilhados abaixo (dentro da gaveta). */} +
{onToggleSidebar && ( )}

AGENDA CLÍNICA

- {(currentRole !== 'paciente' && !souDentista) && ( - - )}
- {/* AGENDAR (desktop, full-width no topo da coluna) */} + {/* AGENDAR (full-width no topo da coluna/gaveta — mobile e desktop) */} {(currentRole !== 'paciente' && !souDentista) && ( - )} - {/* ícones de ação — mesmo tamanho, distribuídos horizontalmente */} -
+ {/* Ações — mobile: empilhadas, ícone + nome, largura cheia; desktop: fileira de ícones */} +
{(currentRole !== 'paciente') && ( - )} {(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'donoconsultorio') && ( )} - {(currentRole === 'admin' || currentRole === 'donoclinica') && ( - )} {souDentista && ( - + )} + {souDentista && ( + )} {(currentRole !== 'paciente' && !souDentista) && ( - )} @@ -747,7 +770,10 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void; sidebar onEditar={() => { setShowDetails(false); setIsModalOpen(true); }} onAtender={handleAtender} onVerWhats={(info) => setWhatsTarget(info)} - podeAtender={!!onNavigate && currentRole !== 'paciente'} + // "Atender" (abre Lançar GTO) é ação CLÍNICA — atende quem trata: dentista, + // biomédico, dono da clínica/consultório, dentista locatário, admin. A recepção + // (funcionario) NÃO atende; ela só remarca/desmarca/agenda. + podeAtender={!!onNavigate && currentRole !== 'paciente' && currentRole !== 'funcionario'} dentistaRestrito={souDentista} /> @@ -861,6 +887,7 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void; sidebar initialPatient={reagendarPatient} /> setIsSettingsOpen(false)} dentists={dentists} /> + setShowMeusHorarios(false)} clinicaId={(HybridBackend.getActiveWorkspace() as any)?.id} initialTab="dentistas" soDentista /> {showInteresses && (
setShowInteresses(false)}>