From 6a95120c5072e30d52239eb5ad32e5bd9fa67cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Gustavo?= <166467972+JoaoGustavo-dev@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:11:58 -0300 Subject: [PATCH] add-delete-appointment-endpoint --- .../app/(main-routes)/consultas/page.tsx | 21 ++- .../forms/calendar-registration-form.tsx | 122 +++++++++++++++--- susconecta/lib/api.ts | 19 +++ 3 files changed, 138 insertions(+), 24 deletions(-) diff --git a/susconecta/app/(main-routes)/consultas/page.tsx b/susconecta/app/(main-routes)/consultas/page.tsx index f0da89a..c33a1dc 100644 --- a/susconecta/app/(main-routes)/consultas/page.tsx +++ b/susconecta/app/(main-routes)/consultas/page.tsx @@ -55,7 +55,7 @@ import { } from "@/components/ui/select"; import { mockProfessionals } from "@/lib/mocks/appointment-mocks"; -import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento, buscarAgendamentoPorId } from "@/lib/api"; +import { listarAgendamentos, buscarPacientesPorIds, buscarMedicosPorIds, atualizarAgendamento, buscarAgendamentoPorId, deletarAgendamento } from "@/lib/api"; import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form"; const formatDate = (date: string | Date) => { @@ -127,9 +127,24 @@ export default function ConsultasPage() { }; }; - const handleDelete = (appointmentId: string) => { - if (window.confirm("Tem certeza que deseja excluir esta consulta?")) { + const handleDelete = async (appointmentId: string) => { + if (!window.confirm("Tem certeza que deseja excluir esta consulta?")) return; + try { + // call server DELETE + await deletarAgendamento(appointmentId); + // remove from UI setAppointments((prev) => prev.filter((a) => a.id !== appointmentId)); + // also update originalAppointments cache + setOriginalAppointments((prev) => (prev || []).filter((a) => a.id !== appointmentId)); + alert('Agendamento excluído com sucesso.'); + } catch (err) { + console.error('[ConsultasPage] Falha ao excluir agendamento', err); + try { + const msg = err instanceof Error ? err.message : String(err); + alert('Falha ao excluir agendamento: ' + msg); + } catch (e) { + // ignore + } } }; diff --git a/susconecta/components/forms/calendar-registration-form.tsx b/susconecta/components/forms/calendar-registration-form.tsx index 245eeaa..cf9f0be 100644 --- a/susconecta/components/forms/calendar-registration-form.tsx +++ b/susconecta/components/forms/calendar-registration-form.tsx @@ -74,8 +74,9 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = const [loadingPatients, setLoadingPatients] = useState(false); const [loadingAssignedDoctors, setLoadingAssignedDoctors] = useState(false); const [loadingPatientsForDoctor, setLoadingPatientsForDoctor] = useState(false); - const [availableSlots, setAvailableSlots] = useState>([]); + const [availableSlots, setAvailableSlots] = useState>([]); const [loadingSlots, setLoadingSlots] = useState(false); + const [lockedDurationFromSlot, setLockedDurationFromSlot] = useState(false); // Helpers to convert between ISO (server) and input[type=datetime-local] value const isoToDatetimeLocal = (iso?: string | null) => { @@ -271,7 +272,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = let mounted = true; setLoadingSlots(true); (async () => { - try { + try { console.debug('[CalendarRegistrationForm] getAvailableSlots - params', { docId, date, appointmentType: formData.appointmentType }); console.debug('[CalendarRegistrationForm] doctorOptions count', (doctorOptions || []).length, 'selectedDoctorId', docId, 'doctorOptions sample', (doctorOptions || []).slice(0,3)); // Build start/end as local day bounds from YYYY-MM-DD to avoid @@ -310,7 +311,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = // Try to restrict the returned slots to the doctor's public availability windows try { - const disponibilidades = await listarDisponibilidades({ doctorId: String(docId) }).catch(() => []); + const disponibilidades = await listarDisponibilidades({ doctorId: String(docId) }).catch(() => []); const weekdayNumber = start.getDay(); // 0 (Sun) .. 6 (Sat) // map weekday number to possible representations (numeric, en, pt, abbrev) const weekdayNames: Record = { @@ -327,7 +328,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = // Filter disponibilidades to those matching the weekday (try multiple fields) const matched = (disponibilidades || []).filter((d: any) => { try { - const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase(); + const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase(); if (!raw) return false; // direct numeric or name match if (allowed.includes(raw)) return true; @@ -352,11 +353,30 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = const e2 = parseTime(d.end_time); const winStart = new Date(start.getFullYear(), start.getMonth(), start.getDate(), s.hh, s.mm, s.ss || 0, 0); const winEnd = new Date(start.getFullYear(), start.getMonth(), start.getDate(), e2.hh, e2.mm, e2.ss || 0, 999); - return { winStart, winEnd }; + const slotMinutes = Number(d.slot_minutes || d.slot_minutes_minutes || null) || null; + return { winStart, winEnd, slotMinutes }; }); + // If any disponibilidade declares slot_minutes, prefill duration_minutes on the form + try { + const candidate = windows.find((w: any) => w.slotMinutes && Number.isFinite(Number(w.slotMinutes))); + if (candidate) { + const durationVal = Number(candidate.slotMinutes); + // Only set if different to avoid unnecessary updates + if ((formData as any).duration_minutes !== durationVal) { + onFormChange({ ...formData, duration_minutes: durationVal }); + } + try { setLockedDurationFromSlot(true); } catch (e) {} + } else { + // no slot_minutes declared -> ensure unlocked + try { setLockedDurationFromSlot(false); } catch (e) {} + } + } catch (e) { + console.debug('[CalendarRegistrationForm] erro ao definir duração automática', e); + } + // Keep backend slots that fall inside windows - const existingInWindow = (av.slots || []).filter((s: any) => { + const existingInWindow = (av.slots || []).filter((s: any) => { try { const sd = new Date(s.datetime); const slotMinutes = sd.getHours() * 60 + sd.getMinutes(); @@ -370,7 +390,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = } catch (e) { return false; } }); - // Determine step (minutes) from returned slots, fallback to 30 + // Determine global step (minutes) from returned slots, fallback to 30 let stepMinutes = 30; try { const times = (av.slots || []).map((s: any) => new Date(s.datetime).getTime()).sort((a: number, b: number) => a - b); @@ -386,18 +406,42 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = // keep fallback } - // Generate slots from windows using stepMinutes, then merge with existingInWindow + // Generate missing slots per window respecting slot_minutes (if present). const generatedSet = new Set(); windows.forEach((w: any) => { try { - // Start at window start rounded to nearest step alignment + const perWindowStep = Number(w.slotMinutes) || stepMinutes; const startMs = w.winStart.getTime(); const endMs = w.winEnd.getTime(); - // We'll generate by advancing stepMinutes - let cursor = new Date(startMs); - while (cursor.getTime() <= endMs) { - generatedSet.add(cursor.toISOString()); - cursor = new Date(cursor.getTime() + stepMinutes * 60000); + // compute last allowed slot start so that start + perWindowStep <= winEnd + const lastStartMs = endMs - perWindowStep * 60000; + + // backend slots inside this window (ms) + const backendSlotsInWindow = (av.slots || []).filter((s: any) => { + try { + const sd = new Date(s.datetime); + const sm = sd.getHours() * 60 + sd.getMinutes(); + const wmStart = w.winStart.getHours() * 60 + w.winStart.getMinutes(); + const wmEnd = w.winEnd.getHours() * 60 + w.winEnd.getMinutes(); + return sm >= wmStart && sm <= wmEnd; + } catch (e) { return false; } + }).map((s: any) => new Date(s.datetime).getTime()).sort((a: number, b: number) => a - b); + + if (!backendSlotsInWindow.length) { + // generate full window from winStart to lastStartMs + let cursorMs = startMs; + while (cursorMs <= lastStartMs) { + generatedSet.add(new Date(cursorMs).toISOString()); + cursorMs += perWindowStep * 60000; + } + } else { + // generate after last backend slot up to lastStartMs + const lastBackendMs = backendSlotsInWindow[backendSlotsInWindow.length - 1]; + let cursorMs = lastBackendMs + perWindowStep * 60000; + while (cursorMs <= lastStartMs) { + generatedSet.add(new Date(cursorMs).toISOString()); + cursorMs += perWindowStep * 60000; + } } } catch (e) { // skip malformed window @@ -405,10 +449,32 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = }); // Merge existingInWindow (prefer backend objects) with generated ones - const mergedMap = new Map(); - (existingInWindow || []).forEach((s: any) => mergedMap.set(s.datetime, s)); + const mergedMap = new Map(); + // helper to find window slotMinutes for a given ISO datetime + const findWindowSlotMinutes = (isoDt: string) => { + try { + const sd = new Date(isoDt); + const sm = sd.getHours() * 60 + sd.getMinutes(); + const w = windows.find((win: any) => { + const ws = win.winStart; + const we = win.winEnd; + const winStartMinutes = ws.getHours() * 60 + ws.getMinutes(); + const winEndMinutes = we.getHours() * 60 + we.getMinutes(); + return sm >= winStartMinutes && sm <= winEndMinutes; + }); + return w && w.slotMinutes ? Number(w.slotMinutes) : null; + } catch (e) { return null; } + }; + + (existingInWindow || []).forEach((s: any) => { + const sm = findWindowSlotMinutes(s.datetime); + mergedMap.set(s.datetime, sm ? { ...s, slot_minutes: sm } : { ...s }); + }); Array.from(generatedSet).forEach((dt) => { - if (!mergedMap.has(dt)) mergedMap.set(dt, { datetime: dt, available: true }); + if (!mergedMap.has(dt)) { + const sm = findWindowSlotMinutes(dt) || stepMinutes; + mergedMap.set(dt, { datetime: dt, available: true, slot_minutes: sm }); + } }); const merged = Array.from(mergedMap.values()).sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime()); @@ -661,7 +727,15 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = const hh = String(dt.getHours()).padStart(2, '0'); const mm = String(dt.getMinutes()).padStart(2, '0'); const dateOnly = dt.toISOString().split('T')[0]; - onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}` }); + // set duration from slot if available + const sel = (availableSlots || []).find((s) => s.datetime === value) as any; + const slotMinutes = sel && sel.slot_minutes ? Number(sel.slot_minutes) : null; + if (slotMinutes) { + onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}`, duration_minutes: slotMinutes }); + try { setLockedDurationFromSlot(true); } catch (e) {} + } else { + onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}` }); + } } catch (e) { // noop } @@ -715,10 +789,16 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode = type="button" className={`h-10 rounded-md border ${formData.startTime === `${hh}:${mm}` ? 'bg-blue-600 text-white' : 'bg-background'}`} onClick={() => { - // when selecting a slot, set appointmentDate (if missing) and startTime + // when selecting a slot, set appointmentDate (if missing) and startTime and duration const isoDate = dt.toISOString(); const dateOnly = isoDate.split('T')[0]; - onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}` }); + const slotMinutes = s.slot_minutes || null; + if (slotMinutes) { + onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}`, duration_minutes: Number(slotMinutes) }); + try { setLockedDurationFromSlot(true); } catch (e) {} + } else { + onFormChange({ ...formData, appointmentDate: dateOnly, startTime: `${hh}:${mm}` }); + } }} > {label} @@ -757,7 +837,7 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
- +
diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 27e58de..c26142d 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1170,6 +1170,25 @@ export async function buscarAgendamentoPorId(id: string | number, select: string throw new Error('404: Agendamento não encontrado'); } +/** + * Deleta um agendamento por ID (DELETE /rest/v1/appointments?id=eq.) + */ +export async function deletarAgendamento(id: string | number): Promise { + if (!id) throw new Error('ID do agendamento é obrigatório'); + const url = `${REST}/appointments?id=eq.${encodeURIComponent(String(id))}`; + // Request minimal return to get a 204 No Content when the delete succeeds. + const res = await fetch(url, { + method: 'DELETE', + headers: withPrefer({ ...baseHeaders() }, 'return=minimal'), + }); + + if (res.status === 204) return; + // Some deployments may return 200 with a representation — accept that too + if (res.status === 200) return; + // Otherwise surface a friendly error using parse() + await parse(res as Response); +} + /** * Buscar relatório por ID (tenta múltiplas estratégias: id, order_number, patient_id) * Retorna o primeiro relatório encontrado ou lança erro 404 quando não achar.