diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx index 504decc..fe97473 100644 --- a/susconecta/app/(main-routes)/consultas/page.tsx +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -74,6 +74,20 @@ const capitalize = (s: string) => { return s.charAt(0).toUpperCase() + s.slice(1); }; +const translateStatus = (status: string) => { + const statusMap: { [key: string]: string } = { + 'requested': 'Solicitado', + 'confirmed': 'Confirmado', + 'checked_in': 'Check-in', + 'in_progress': 'Em Andamento', + 'completed': 'Concluído', + 'cancelled': 'Cancelado', + 'no_show': 'Não Compareceu', + 'pending': 'Pendente', + }; + return statusMap[status?.toLowerCase()] || capitalize(status || ''); +}; + export default function ConsultasPage() { const [appointments, setAppointments] = useState([]); const [originalAppointments, setOriginalAppointments] = useState([]); @@ -197,7 +211,7 @@ export default function ConsultasPage() { const payload: any = { scheduled_at, duration_minutes, - status: formData.status || undefined, + status: 'confirmed', notes: formData.notes ?? null, chief_complaint: formData.chief_complaint ?? null, patient_notes: formData.patient_notes ?? null, @@ -561,7 +575,7 @@ export default function ConsultasPage() { } className={appointment.status === "confirmed" ? "bg-green-600" : ""} > - {capitalize(appointment.status)} + {translateStatus(appointment.status)} {formatDate(appointment.scheduled_at ?? appointment.time)} @@ -652,7 +666,7 @@ export default function ConsultasPage() { } className={`text-[10px] sm:text-xs ${appointment.status === "confirmed" ? "bg-green-600" : ""}`} > - {capitalize(appointment.status)} + {translateStatus(appointment.status)}
@@ -771,7 +785,7 @@ export default function ConsultasPage() { } className={viewingAppointment?.status === "confirmed" ? "bg-green-600" : ""} > - {capitalize(viewingAppointment?.status || "")} + {translateStatus(viewingAppointment?.status || "")}
diff --git a/susconecta/app/(main-routes)/perfil/page.tsx b/susconecta/app/(main-routes)/perfil/page.tsx index 0308815..e5c7796 100644 --- a/susconecta/app/(main-routes)/perfil/page.tsx +++ b/susconecta/app/(main-routes)/perfil/page.tsx @@ -51,7 +51,7 @@ interface UserProfile { export default function PerfilPage() { const router = useRouter(); - const { user: authUser } = useAuth(); + const { user: authUser, updateUserProfile } = useAuth(); const [userInfo, setUserInfo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -229,6 +229,31 @@ export default function PerfilPage() { } : null, } : null ); + + // Also update global auth profile so header/avatar updates immediately + try { + if (typeof updateUserProfile === 'function') { + updateUserProfile({ + // Persist common keys used across the app + foto_url: editingData.avatar_url || undefined, + telefone: editingData.phone || undefined + }); + } else { + // Fallback: try to persist directly to localStorage so next reload shows it + try { + const raw = localStorage.getItem('auth_user') + if (raw) { + const u = JSON.parse(raw) + u.profile = u.profile || {} + if (editingData.avatar_url) { u.profile.foto_url = editingData.avatar_url; u.profile.avatar_url = editingData.avatar_url } + if (editingData.phone) u.profile.telefone = editingData.phone + localStorage.setItem('auth_user', JSON.stringify(u)) + } + } catch (_e) {} + } + } catch (err) { + console.warn('[PERFIL] Falha ao sincronizar profile global:', err) + } } catch (err: any) { console.error('[PERFIL] Erro ao salvar:', err); } @@ -296,7 +321,7 @@ export default function PerfilPage() { className="bg-blue-600 hover:bg-blue-700" onClick={handleEditClick} > - ✏️ Editar Perfil + Editar Perfil ) : (
@@ -591,7 +616,25 @@ export default function PerfilPage() { setEditingData({...editingData, avatar_url: newUrl})} + onAvatarChange={(newUrl) => { + setEditingData({...editingData, avatar_url: newUrl}) + try { + if (typeof updateUserProfile === 'function') { + updateUserProfile({ foto_url: newUrl }) + } else { + const raw = localStorage.getItem('auth_user') + if (raw) { + const u = JSON.parse(raw) + u.profile = u.profile || {} + u.profile.foto_url = newUrl + u.profile.avatar_url = newUrl + localStorage.setItem('auth_user', JSON.stringify(u)) + } + } + } catch (err) { + console.warn('[PERFIL] erro ao persistir avatar no auth_user localStorage', err) + } + }} userName={editingData.full_name || userInfo.profile?.full_name || "Usuário"} />
diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index d8ea7ef..f7ab954 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -826,7 +826,7 @@ export default function PacientePage() {
@@ -851,10 +851,8 @@ export default function PacientePage() { {/* Status Badge */}
{statusLabel(consulta.status)} @@ -872,7 +870,7 @@ export default function PacientePage() { Detalhes {/* Reagendar removed by request */} - {consulta.status !== 'Cancelada' && ( + {consulta.status !== 'Cancelada' && consulta.status !== 'cancelled' && ( ) : (
diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 1296ed3..c3c362d 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -2766,7 +2766,7 @@ const ProfissionalPage = () => { className="bg-blue-600 hover:bg-blue-700 text-xs sm:text-sm w-full sm:w-auto" onClick={() => setIsEditingProfile(true)} > - ✏️ Editar Perfil + Editar Perfil ) : (
diff --git a/susconecta/components/features/dashboard/header.tsx b/susconecta/components/features/dashboard/header.tsx index c9587cf..5b78f26 100644 --- a/susconecta/components/features/dashboard/header.tsx +++ b/susconecta/components/features/dashboard/header.tsx @@ -60,9 +60,32 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary" onClick={() => setDropdownOpen(!dropdownOpen)} > + {/* Mostrar foto do usuário quando disponível; senão, mostrar fallback com iniciais */} - - RA + { + (() => { + const userPhoto = (user as any)?.profile?.foto_url || (user as any)?.profile?.fotoUrl || (user as any)?.profile?.avatar_url + const alt = user?.name || user?.email || 'Usuário' + + const getInitials = (name?: string, email?: string) => { + if (name) { + const parts = name.trim().split(/\s+/) + const first = parts[0]?.charAt(0) ?? '' + const second = parts[1]?.charAt(0) ?? '' + return (first + second).toUpperCase() || (email?.charAt(0) ?? 'U').toUpperCase() + } + if (email) return email.charAt(0).toUpperCase() + return 'U' + } + + return ( + <> + + {getInitials(user?.name, user?.email)} + + ) + })() + } @@ -94,11 +117,9 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub }} className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer" > - 👤 Perfil - - +
diff --git a/susconecta/components/features/forms/calendar-registration-form.tsx b/susconecta/components/features/forms/calendar-registration-form.tsx index 1edba87..afc2aea 100644 --- a/susconecta/components/features/forms/calendar-registration-form.tsx +++ b/susconecta/components/features/forms/calendar-registration-form.tsx @@ -2,7 +2,7 @@ "use client"; import { useState, useEffect, useRef } from "react"; -import { buscarPacientePorId, listarMedicos, buscarPacientesPorMedico, getAvailableSlots, buscarPacientes, listarPacientes, listarDisponibilidades, listarExcecoes } from "@/lib/api"; +import { buscarPacientePorId, listarMedicos, buscarPacientesPorMedico, getAvailableSlots, buscarPacientes, listarPacientes, listarDisponibilidades, listarExcecoes, listarAgendamentos } from "@/lib/api"; import { toast } from '@/hooks/use-toast'; import { AlertDialog, @@ -93,6 +93,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = const [exceptionDialogOpen, setExceptionDialogOpen] = useState(false); const [exceptionDialogMessage, setExceptionDialogMessage] = useState(null); const [showDatePicker, setShowDatePicker] = useState(false); + const [bookedSlots, setBookedSlots] = useState>(new Set()); // ISO datetimes of already booked appointments // Helpers to convert between ISO (server) and input[type=datetime-local] value const isoToDatetimeLocal = (iso?: string | null) => { @@ -298,31 +299,9 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = if (mountedRef.current) setLoadingSlots(true); try { - // Check for blocking exceptions first - try { - const exceptions = await listarExcecoes({ doctorId: String(docId), date: String(date) }).catch(() => []); - if (exceptions && exceptions.length) { - const blocking = (exceptions || []).find((e: any) => e && e.kind === 'bloqueio'); - if (blocking) { - const reason = blocking.reason ? ` Motivo: ${blocking.reason}` : ''; - const msg = `Não é possível agendar nesta data.${reason}`; - try { - setExceptionDialogMessage(msg); - setExceptionDialogOpen(true); - } catch (e) { - try { toast({ title: 'Data indisponível', description: msg }); } catch (ee) {} - } - if (mountedRef.current) { - setAvailableSlots([]); - setLoadingSlots(false); - } - return; - } - } - } catch (exCheckErr) { - console.warn('[CalendarRegistrationForm] listarExcecoes falhou, continuando para getAvailableSlots', exCheckErr); - } - + // Skip exception checking - all dates are available for admin now + // NOTE: Exception checking disabled per user request + console.debug('[CalendarRegistrationForm] getAvailableSlots - params', { docId, date, appointmentType: formData.appointmentType }); // Build local start/end for the day @@ -556,7 +535,61 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Filter available slots: if date is today, only show future times + // Load already booked appointments for the selected doctor and date to prevent double-booking + useEffect(() => { + const docId = (formData as any).doctorId || (formData as any).doctor_id || null; + const date = (formData as any).appointmentDate || null; + + if (!docId || !date) { + setBookedSlots(new Set()); + return; + } + + let mounted = true; + (async () => { + try { + // Query appointments for this doctor on the selected date + // Format: YYYY-MM-DD + const [y, m, d] = String(date).split('-').map(n => Number(n)); + const dateStart = new Date(y, m - 1, d, 0, 0, 0, 0).toISOString(); + const dateEnd = new Date(y, m - 1, d, 23, 59, 59, 999).toISOString(); + + const query = `doctor_id=eq.${docId}&scheduled_at=gte.${dateStart}&scheduled_at=lte.${dateEnd}&select=scheduled_at`; + const appointments = await listarAgendamentos(query).catch(() => []); + + if (!mounted) return; + + // Extract booked datetime slots - store as HH:MM format for easier comparison + const booked = new Set(); + (appointments || []).forEach((appt: any) => { + if (appt && appt.scheduled_at) { + try { + const dt = new Date(appt.scheduled_at); + const hh = String(dt.getHours()).padStart(2, '0'); + const mm = String(dt.getMinutes()).padStart(2, '0'); + const timeKey = `${hh}:${mm}`; + booked.add(timeKey); + console.debug('[CalendarRegistrationForm] booked time:', timeKey, 'from', appt.scheduled_at); + } catch (e) { + console.warn('[CalendarRegistrationForm] erro parsing scheduled_at', appt.scheduled_at, e); + } + } + }); + + if (mounted) { + setBookedSlots(booked); + console.debug('[CalendarRegistrationForm] total booked slots:', booked.size, 'slots:', Array.from(booked)); + } + } catch (e) { + console.warn('[CalendarRegistrationForm] erro ao carregar agendamentos existentes', e); + if (mounted) setBookedSlots(new Set()); + } + })(); + + return () => { mounted = false; }; + }, [(formData as any).doctorId, (formData as any).doctor_id, (formData as any).appointmentDate]); + + // Filter available slots: if date is today, only show future times, AND remove already booked slots const filteredAvailableSlots = (() => { try { const now = new Date(); @@ -566,9 +599,11 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = const currentMinutes = now.getMinutes(); const currentTimeInMinutes = currentHours * 60 + currentMinutes; + let filtered = availableSlots || []; + if (selectedDateStr === todayStr) { // Today: filter out past times (add 30-minute buffer for admin to schedule) - return (availableSlots || []).filter((s) => { + filtered = (availableSlots || []).filter((s) => { try { const slotDate = new Date(s.datetime); const slotHours = slotDate.getHours(); @@ -582,11 +617,29 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = }); } else if (selectedDateStr && selectedDateStr > todayStr) { // Future date: show all slots - return availableSlots || []; + filtered = availableSlots || []; } else { // Past date: no slots return []; } + + // Remove already booked slots - compare by HH:MM format + return filtered.filter((s) => { + try { + const dt = new Date(s.datetime); + const hh = String(dt.getHours()).padStart(2, '0'); + const mm = String(dt.getMinutes()).padStart(2, '0'); + const timeKey = `${hh}:${mm}`; + const isBooked = bookedSlots.has(timeKey); + if (isBooked) { + console.debug('[CalendarRegistrationForm] filtering out booked slot:', timeKey); + } + return !isBooked; + } catch (e) { + console.warn('[CalendarRegistrationForm] erro filtering booked slot', e); + return true; + } + }); } catch (e) { return availableSlots || []; } @@ -1178,15 +1231,8 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
- - - - - -
diff --git a/susconecta/hooks/useAuth.tsx b/susconecta/hooks/useAuth.tsx index 1725b99..a2bec86 100644 --- a/susconecta/hooks/useAuth.tsx +++ b/susconecta/hooks/useAuth.tsx @@ -336,6 +336,26 @@ export function AuthProvider({ children }: { children: ReactNode }) { } }, [user?.userType, token, clearAuthData]) + // Allow updating the in-memory user profile and persist to localStorage. + const updateUserProfile = useCallback((partial: Partial) => { + try { + setUser((prev) => { + if (!prev) return prev + const next = { ...prev, profile: { ...(prev.profile || {}), ...(partial || {}) } } + try { + if (typeof window !== 'undefined') { + localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(next)) + } + } catch (e) { + console.warn('[AUTH] Falha ao persistir user atualizado no localStorage:', e) + } + return next + }) + } catch (err) { + console.warn('[AUTH] updateUserProfile erro:', err) + } + }, []) + // Refresh token memoizado (usado pelo HTTP client) const refreshToken = useCallback(async (): Promise => { // Esta função é principalmente para compatibilidade @@ -350,8 +370,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { token, login, logout, - refreshToken - }), [authStatus, user, token, login, logout, refreshToken]) + refreshToken, + updateUserProfile + }), [authStatus, user, token, login, logout, refreshToken, updateUserProfile]) // Inicialização única useEffect(() => { diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 18f8976..bbf3c9b 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1086,66 +1086,24 @@ export async function criarAgendamento(input: AppointmentCreate): Promise { ... }); + // if (!matching) throw new Error(...); - const matching = (av.slots || []).find((s) => { - try { - const dt = new Date(s.datetime).getTime(); - // allow small tolerance (<= 60s) to account for formatting/timezone differences - return s.available && Math.abs(dt - scheduledMs) <= 60_000; - } catch (e) { - return false; - } - }); - - if (!matching) { - throw new Error('Horário não disponível para o médico no horário solicitado. Verifique a disponibilidade antes de agendar.'); - } - - // --- Prevent creating an appointment on a date with a blocking exception --- + // --- Skip exception checking for admin - allow all dates and times --- + // NOTE: Exception validation disabled per user request + /* try { - // listarExcecoes can filter by date const dateOnly = startDay.toISOString().split('T')[0]; const exceptions = await listarExcecoes({ doctorId: input.doctor_id, date: dateOnly }).catch(() => []); - if (exceptions && exceptions.length) { - for (const ex of exceptions) { - try { - if (!ex || !ex.kind) continue; - if (ex.kind !== 'bloqueio') continue; - // If no start_time/end_time -> blocks whole day - if (!ex.start_time && !ex.end_time) { - const reason = ex.reason ? ` Motivo: ${ex.reason}` : ''; - throw new Error(`Não é possível agendar para esta data. Existe uma exceção que bloqueia o dia.${reason}`); - } - // Otherwise check overlap with scheduled time - // Parse exception times and scheduled time to minutes - const parseToMinutes = (t?: string | null) => { - if (!t) return null; - const parts = String(t).split(':').map(Number); - if (parts.length >= 2 && !Number.isNaN(parts[0]) && !Number.isNaN(parts[1])) return parts[0] * 60 + parts[1]; - return null; - }; - const exStart = parseToMinutes(ex.start_time ?? undefined); - const exEnd = parseToMinutes(ex.end_time ?? undefined); - const sched = new Date(input.scheduled_at); - const schedMinutes = sched.getHours() * 60 + sched.getMinutes(); - const schedDuration = input.duration_minutes ?? 30; - const schedEndMinutes = schedMinutes + Number(schedDuration); - if (exStart != null && exEnd != null && schedMinutes < exEnd && exStart < schedEndMinutes) { - const reason = ex.reason ? ` Motivo: ${ex.reason}` : ''; - throw new Error(`Não é possível agendar neste horário por uma exceção que bloqueia parte do dia.${reason}`); - } - } catch (inner) { - // Propagate the exception as user-facing error - throw inner; - } - } - } + // ... exception checking logic removed ... } catch (e) { if (e instanceof Error) throw e; } + */ // Determine created_by similar to other creators (prefer localStorage then user-info) let createdBy: string | null = null; @@ -1175,6 +1133,7 @@ export async function criarAgendamento(input: AppointmentCreate): Promise Promise logout: () => Promise refreshToken: () => Promise + // Merge partial profile into the stored user (client-side convenience) + updateUserProfile?: (partial: Partial) => void } export interface AuthStorageKeys {