From 7cfd14ae0933a809fd62c07cad08ba447d02bdbe Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Wed, 8 Jul 2026 07:46:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(timezone):=20exibi=C3=A7=C3=A3o=20no=20fus?= =?UTF-8?q?o=20de=20armazenamento=20=E2=80=94=20corrige=20o=20-1h=20(Fase?= =?UTF-8?q?=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit start_time é naive; o node-pg o serializa no fuso do container (America/Sao_Paulo). Exibir no fuso do NAVEGADOR (ex.: UTC-4) mostrava -1h (09:30 -> 08:30). Agora o frontend formata SEMPRE em America/Sao_Paulo (o mesmo frame do armazenamento), recuperando o wall-clock literal para qualquer navegador: - utils/appTime.ts: STORAGE_TZ + fmtHora/fmtDataCurta/fmtDataHora. - FullCalendar timeZone="America/Sao_Paulo". - AgendaDetailModal (card "Data e hora" + histórico) e AgendaView (interesses) usam os helpers. Co-Authored-By: Claude Opus 4.8 --- frontend/components/AgendaDetailModal.tsx | 13 +++++++------ frontend/utils/appTime.ts | 18 ++++++++++++++++++ frontend/views/AgendaView.tsx | 4 +++- 3 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 frontend/utils/appTime.ts diff --git a/frontend/components/AgendaDetailModal.tsx b/frontend/components/AgendaDetailModal.tsx index c748940..fbaa05a 100644 --- a/frontend/components/AgendaDetailModal.tsx +++ b/frontend/components/AgendaDetailModal.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { StackModal, StackPage, useStackModal } from './StackModal.tsx'; +import { fmtHora, fmtDataCurta, fmtDataHora } from '../utils/appTime.ts'; import { HybridBackend } from '../services/backend.ts'; import { useToast } from '../contexts/ToastContext.tsx'; import { AvaliacaoEditor } from './AvaliacaoEditor.tsx'; @@ -137,9 +138,9 @@ const HistoryPage: React.FC<{ appointmentId: string }> = ({ appointmentId }) =>

{new Date(h.at).toLocaleString('pt-BR')}

{h.detalhes?.de && h.detalhes?.para && (

- {new Date(h.detalhes.de.start).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} + {fmtDataHora(h.detalhes.de.start)} {' → '} - {new Date(h.detalhes.para.start).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} + {fmtDataHora(h.detalhes.para.start)}

)} @@ -304,8 +305,8 @@ const DentistaRestritoView: React.FC<{ ag: any; nome: string; dentNome: string;
-

{ag.start ? new Date(ag.start).toLocaleDateString('pt-BR', { weekday: 'short', day: 'numeric', month: 'short' }) : '—'}

-

{ag.start ? new Date(ag.start).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '—'}

+

{ag.start ? fmtDataCurta(ag.start) : '—'}

+

{ag.start ? fmtHora(ag.start) : '—'}

@@ -421,8 +422,8 @@ const DetailPage: React.FC = (p) => {
-

{ag.start ? new Date(ag.start).toLocaleDateString('pt-BR', { weekday: 'short', day: 'numeric', month: 'short' }) : '—'}

-

{ag.start ? new Date(ag.start).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : '—'}

+

{ag.start ? fmtDataCurta(ag.start) : '—'}

+

{ag.start ? fmtHora(ag.start) : '—'}

diff --git a/frontend/utils/appTime.ts b/frontend/utils/appTime.ts new file mode 100644 index 0000000..99b54fe --- /dev/null +++ b/frontend/utils/appTime.ts @@ -0,0 +1,18 @@ +// Fuso de REFERÊNCIA do armazenamento. As colunas de horário (agendamentos.start_time) +// são `timestamp` naive; o backend (container) roda em America/Sao_Paulo, então o node-pg +// serializa o wall-clock naive nesse fuso. Para exibir o horário LITERAL (o mesmo que a +// Secretária ofereceu/gravou) em qualquer navegador, formatamos SEMPRE neste fuso — e não +// no fuso local do navegador (que causaria o deslocamento de ±1h). +export const STORAGE_TZ = 'America/Sao_Paulo'; + +const D = (v?: string | number | Date | null) => (v === null || v === undefined || v === '' ? null : new Date(v)); + +export const fmtHora = (v?: string | number | Date | null): string => { + const d = D(v); return d ? d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', timeZone: STORAGE_TZ }) : '—'; +}; +export const fmtDataCurta = (v?: string | number | Date | null): string => { + const d = D(v); return d ? d.toLocaleDateString('pt-BR', { weekday: 'short', day: 'numeric', month: 'short', timeZone: STORAGE_TZ }) : '—'; +}; +export const fmtDataHora = (v?: string | number | Date | null): string => { + const d = D(v); return d ? d.toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', timeZone: STORAGE_TZ }) : '—'; +}; diff --git a/frontend/views/AgendaView.tsx b/frontend/views/AgendaView.tsx index 2956873..13ca5cd 100644 --- a/frontend/views/AgendaView.tsx +++ b/frontend/views/AgendaView.tsx @@ -3,6 +3,7 @@ import FullCalendar from '@fullcalendar/react'; import dayGridPlugin from '@fullcalendar/daygrid'; import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; +import { fmtDataHora } from '../utils/appTime.ts'; import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2, Bell, CalendarClock, History, Download, PanelLeftClose, PanelLeftOpen } from 'lucide-react'; import { AgendaSettingsModal } from '../components/AgendaSettingsModal.tsx'; import { HorariosConfig } from '../components/HorariosConfig.tsx'; @@ -749,6 +750,7 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void; sidebar void; sidebar

- {new Date(i.start_time).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })} + {fmtDataHora(i.start_time)}

{i.status}