develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
2 changed files with 93 additions and 82 deletions
Showing only changes of commit dce7b485e2 - Show all commits

View File

@ -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<string | null>(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const [bookedSlots, setBookedSlots] = useState<Set<string>>(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,30 +299,8 @@ 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 });
@ -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<string>();
(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 || [];
}

View File

@ -1086,66 +1086,24 @@ export async function criarAgendamento(input: AppointmentCreate): Promise<Appoin
const endDay = new Date(scheduledDate);
endDay.setHours(23, 59, 59, 999);
// Query availability
const av = await getAvailableSlots({ doctor_id: input.doctor_id, start_date: startDay.toISOString(), end_date: endDay.toISOString(), appointment_type: input.appointment_type });
const scheduledMs = scheduledDate.getTime();
// Skip availability check for admin - allow any time to be scheduled
// NOTE: Availability validation disabled per user request
// const av = await getAvailableSlots({ doctor_id: input.doctor_id, start_date: startDay.toISOString(), end_date: endDay.toISOString(), appointment_type: input.appointment_type });
// const scheduledMs = scheduledDate.getTime();
// const matching = (av.slots || []).find((s) => { ... });
// 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;