Agendar e listar consultas na página de página de paciente

This commit is contained in:
Lucas Deiró Rodrigues 2025-11-07 02:18:02 -03:00
parent 805aa66f6f
commit e1da45c74d
5 changed files with 400 additions and 199 deletions

View File

@ -4,19 +4,19 @@ import { usersService } from "services/usersApi.mjs";
import { doctorsService } from "services/doctorsApi.mjs"; import { doctorsService } from "services/doctorsApi.mjs";
import { appointmentsService } from "services/appointmentsApi.mjs"; import { appointmentsService } from "services/appointmentsApi.mjs";
import { AvailabilityService } from "services/availabilityApi.mjs"; import { AvailabilityService } from "services/availabilityApi.mjs";
import { Calendar as CalendarShadcn } from "@/components/ui/calendar";
import { format, addDays } from "date-fns";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { Calendar, Clock, User } from "lucide-react"; import { Calendar, Clock, User, StickyNote } from "lucide-react";
import PatientLayout from "@/components/patient-layout"; import PatientLayout from "@/components/patient-layout";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
Select, Select,
@ -26,8 +26,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/hooks/use-toast";
const API_URL = " https://yuanqfswhberkoevtmfr.supabase.co/";
interface Doctor { interface Doctor {
id: string; id: string;
@ -36,6 +36,8 @@ interface Doctor {
} }
interface Disponibilidade { interface Disponibilidade {
id?: string;
doctor_id?: string;
weekday: string; weekday: string;
start_time: string; start_time: string;
end_time: string; end_time: string;
@ -50,11 +52,26 @@ export default function ScheduleAppointment() {
const [availableTimes, setAvailableTimes] = useState<string[]>([]); const [availableTimes, setAvailableTimes] = useState<string[]>([]);
const [loadingSlots, setLoadingSlots] = useState(false); const [loadingSlots, setLoadingSlots] = useState(false);
const [loadingDoctors, setLoadingDoctors] = useState(true); const [loadingDoctors, setLoadingDoctors] = useState(true);
const [tipoConsulta, setTipoConsulta] = useState("presencial"); const [tipoConsulta, setTipoConsulta] = useState("presencial");
const [duracao, setDuracao] = useState("30"); const [duracao, setDuracao] = useState("30");
const [notes, setNotes] = useState(""); const [notes, setNotes] = useState("");
const [disponibilidades, setDisponibilidades] = useState<Disponibilidade[]>([]);
const [availableWeekdays, setAvailableWeekdays] = useState<number[]>([]); // 1..7
const [availabilityCounts, setAvailabilityCounts] = useState<Record<string, number>>({}); // "yyyy-MM-dd" -> count
const calendarRef = useRef<HTMLDivElement | null>(null);
const tooltipRef = useRef<HTMLDivElement | null>(null);
const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null);
// --- Helpers ---
const getWeekdayNumber = (weekday: string) =>
["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
.indexOf(weekday.toLowerCase()) + 1; // monday=1 ... sunday=7
const getBrazilDate = (date: Date) =>
new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0));
// --- Fetch doctors ---
const fetchDoctors = useCallback(async () => { const fetchDoctors = useCallback(async () => {
setLoadingDoctors(true); setLoadingDoctors(true);
try { try {
@ -62,11 +79,115 @@ export default function ScheduleAppointment() {
setDoctors(data || []); setDoctors(data || []);
} catch (e) { } catch (e) {
console.error("Erro ao buscar médicos:", e); console.error("Erro ao buscar médicos:", e);
toast({ title: "Erro", description: "Não foi possível carregar médicos." });
} finally { } finally {
setLoadingDoctors(false); setLoadingDoctors(false);
} }
}, []); }, []);
// --- Load disponibilidades details for selected doctor and compute weekdays ---
const loadDoctorDisponibilidades = useCallback(async (doctorId?: string) => {
if (!doctorId) {
setDisponibilidades([]);
setAvailableWeekdays([]);
setAvailabilityCounts({});
return;
}
try {
const disp: Disponibilidade[] = await AvailabilityService.listById(doctorId);
setDisponibilidades(disp || []);
const nums = (disp || []).map((d) => getWeekdayNumber(d.weekday)).filter(Boolean);
setAvailableWeekdays(Array.from(new Set(nums)));
// compute counts preview for next 90 days
await computeAvailabilityCountsPreview(doctorId, disp || []);
} catch (e) {
console.error("Erro disponibilidades:", e);
setDisponibilidades([]);
setAvailableWeekdays([]);
setAvailabilityCounts({});
}
}, []);
// --- Compute availability counts for next 90 days (efficient) ---
const computeAvailabilityCountsPreview = async (doctorId: string, dispList: Disponibilidade[]) => {
try {
const today = new Date();
const start = format(today, "yyyy-MM-dd");
const endDate = addDays(today, 90);
const end = format(endDate, "yyyy-MM-dd");
// fetch appointments for this doctor for the whole window (one call)
const appointments = await appointmentsService.search_appointment(
`doctor_id=eq.${doctorId}&scheduled_at=gte.${start}T00:00:00Z&scheduled_at=lt.${end}T23:59:59Z`
);
// group appointments by date
const apptsByDate: Record<string, number> = {};
(appointments || []).forEach((a: any) => {
const d = String(a.scheduled_at).split("T")[0];
apptsByDate[d] = (apptsByDate[d] || 0) + 1;
});
const counts: Record<string, number> = {};
for (let i = 0; i <= 90; i++) {
const d = addDays(today, i);
const key = format(d, "yyyy-MM-dd");
const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay(); // 1..7
// find all disponibilidades matching this weekday
const dailyDisp = dispList.filter((p) => getWeekdayNumber(p.weekday) === dayOfWeek);
if (dailyDisp.length === 0) {
counts[key] = 0;
continue;
}
// compute total possible slots for the day summing multiple intervals
let possible = 0;
dailyDisp.forEach((p) => {
const [sh, sm] = p.start_time.split(":").map(Number);
const [eh, em] = p.end_time.split(":").map(Number);
const startMin = sh * 60 + sm;
const endMin = eh * 60 + em;
const slot = p.slot_minutes || 30;
// inclusive handling: if start==end -> 1 slot? normally not, we do Math.floor((end - start)/slot) + 1 if end >= start
if (endMin >= startMin) {
possible += Math.floor((endMin - startMin) / slot) + 1;
}
});
const occupied = apptsByDate[key] || 0;
const free = Math.max(0, possible - occupied);
counts[key] = free;
}
setAvailabilityCounts(counts);
} catch (e) {
console.error("Erro ao calcular contagens de disponibilidade:", e);
setAvailabilityCounts({});
}
};
// --- When doctor changes ---
useEffect(() => {
fetchDoctors();
}, [fetchDoctors]);
useEffect(() => {
if (selectedDoctor) {
loadDoctorDisponibilidades(selectedDoctor);
} else {
setDisponibilidades([]);
setAvailableWeekdays([]);
setAvailabilityCounts({});
}
setSelectedDate("");
setSelectedTime("");
setAvailableTimes([]);
}, [selectedDoctor, loadDoctorDisponibilidades]);
// --- Fetch available times for date --- (same logic, but shows toast if none)
const fetchAvailableSlots = useCallback( const fetchAvailableSlots = useCallback(
async (doctorId: string, date: string) => { async (doctorId: string, date: string) => {
if (!doctorId || !date) return; if (!doctorId || !date) return;
@ -74,36 +195,28 @@ export default function ScheduleAppointment() {
setAvailableTimes([]); setAvailableTimes([]);
try { try {
const disponibilidades: Disponibilidade[] = const disponibilidades: Disponibilidade[] = await AvailabilityService.listById(doctorId);
await AvailabilityService.listById(doctorId);
const consultas = await appointmentsService.search_appointment( const consultas = await appointmentsService.search_appointment(
`doctor_id=eq.${doctorId}&scheduled_at=gte.${date}&scheduled_at=lt.${date}T23:59:59` `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z`
); );
const diaJS = new Date(date).getDay(); const diaJS = new Date(date).getDay(); // 0..6
// Ajuste: Sunday = 0 -> API pode esperar 1-7
const diaAPI = diaJS === 0 ? 7 : diaJS; const diaAPI = diaJS === 0 ? 7 : diaJS;
console.log("Disponibilidades recebidas: ", disponibilidades);
console.log("Consultas do dia: ", consultas);
const disponibilidadeDia = disponibilidades.find( const disponibilidadeDia = disponibilidades.find(
(d: Disponibilidade) => Number(diaAPI) === getWeekdayNumber(d.weekday) (d) => getWeekdayNumber(d.weekday) === diaAPI
); );
if (!disponibilidadeDia) { if (!disponibilidadeDia) {
console.log("Nenhuma disponibilidade para este dia");
setAvailableTimes([]); setAvailableTimes([]);
toast({ title: "Nenhuma disponibilidade", description: "Nenhuma disponibilidade cadastrada para este dia." });
setLoadingSlots(false); setLoadingSlots(false);
return; return;
} }
const [startHour, startMin] = disponibilidadeDia.start_time const [startHour, startMin] = disponibilidadeDia.start_time.split(":").map(Number);
.split(":") const [endHour, endMin] = disponibilidadeDia.end_time.split(":").map(Number);
.map(Number);
const [endHour, endMin] = disponibilidadeDia.end_time
.split(":")
.map(Number);
const slot = disponibilidadeDia.slot_minutes || 30; const slot = disponibilidadeDia.slot_minutes || 30;
const horariosGerados: string[] = []; const horariosGerados: string[] = [];
@ -113,20 +226,26 @@ export default function ScheduleAppointment() {
const end = new Date(date); const end = new Date(date);
end.setHours(endHour, endMin, 0, 0); end.setHours(endHour, endMin, 0, 0);
while (atual < end) { while (atual <= end) {
horariosGerados.push(atual.toTimeString().slice(0, 5)); horariosGerados.push(atual.toTimeString().slice(0, 5));
atual = new Date(atual.getTime() + slot * 60 * 1000); atual = new Date(atual.getTime() + slot * 60000);
} }
const ocupados = consultas.map((c: any) => const ocupados = (consultas || []).map((c: any) =>
c.scheduled_at.split("T")[1].slice(0, 5) String(c.scheduled_at).split("T")[1]?.slice(0, 5)
); );
const livres = horariosGerados.filter((h) => !ocupados.includes(h)); const livres = horariosGerados.filter((h) => !ocupados.includes(h));
if (livres.length === 0) {
toast({ title: "Sem horários livres", description: "Todos os horários estão ocupados neste dia." });
}
setAvailableTimes(livres); setAvailableTimes(livres);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setAvailableTimes([]); setAvailableTimes([]);
toast({ title: "Erro", description: "Falha ao carregar horários." });
} finally { } finally {
setLoadingSlots(false); setLoadingSlots(false);
} }
@ -134,32 +253,7 @@ export default function ScheduleAppointment() {
[] []
); );
const getWeekdayNumber = (weekday: string) => { // run fetchAvailableSlots when date changes
// Converte weekday API para número: 1=Monday ... 7=Sunday
switch (weekday.toLowerCase()) {
case "monday":
return 1;
case "tuesday":
return 2;
case "wednesday":
return 3;
case "thursday":
return 4;
case "friday":
return 5;
case "saturday":
return 6;
case "sunday":
return 7;
default:
return 0;
}
};
useEffect(() => {
fetchDoctors();
}, [fetchDoctors]);
useEffect(() => { useEffect(() => {
if (selectedDoctor && selectedDate) { if (selectedDoctor && selectedDate) {
fetchAvailableSlots(selectedDoctor, selectedDate); fetchAvailableSlots(selectedDoctor, selectedDate);
@ -169,193 +263,292 @@ export default function ScheduleAppointment() {
setSelectedTime(""); setSelectedTime("");
}, [selectedDoctor, selectedDate, fetchAvailableSlots]); }, [selectedDoctor, selectedDate, fetchAvailableSlots]);
// --- Submit ---
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!selectedDoctor || !selectedDate || !selectedTime) { if (!selectedDoctor || !selectedDate || !selectedTime) {
alert("Selecione médico, data e horário."); toast({ title: "Preencha os campos", description: "Selecione médico, data e horário." });
return; return;
} }
const doctor = doctors.find((d) => d.id === selectedDoctor);
const scheduledISO = `${selectedDate}T${selectedTime}:00Z`;
const paciente = await usersService.getMe();
const body = {
doctor_id: doctor?.id,
patient_id: paciente.user.id,
scheduled_at: scheduledISO,
duration_minutes: Number(duracao),
created_by: paciente.user.id,
};
try { try {
const res = await fetch(`${API_URL}/appointments`, { const doctor = doctors.find((d) => d.id === selectedDoctor);
method: "POST", const paciente = await usersService.getMe();
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error("Erro ao agendar consulta"); const body = {
doctor_id: doctor?.id,
patient_id: paciente.user.id,
scheduled_at: `${selectedDate}T${selectedTime}:00`, // saving as local-ish string (you chose UTC elsewhere)
duration_minutes: Number(duracao),
notes,
appointment_type: tipoConsulta,
};
alert("Consulta agendada com sucesso!"); await appointmentsService.create(body);
toast({ title: "Agendado", description: "Consulta agendada com sucesso." });
// reset
setSelectedDoctor(""); setSelectedDoctor("");
setSelectedDate(""); setSelectedDate("");
setSelectedTime(""); setSelectedTime("");
setAvailableTimes([]); setAvailableTimes([]);
setNotes("");
// refresh counts
if (selectedDoctor) computeAvailabilityCountsPreview(selectedDoctor, disponibilidades);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert("Falha ao agendar consulta"); toast({ title: "Erro", description: "Falha ao agendar consulta." });
} }
}; };
// --- Calendar tooltip via event delegation ---
useEffect(() => {
const cont = calendarRef.current;
if (!cont) return;
const onMove = (ev: MouseEvent) => {
const target = ev.target as HTMLElement | null;
if (!target) return;
// find closest button that likely is a day cell
const btn = target.closest("button");
if (!btn) {
setTooltip(null);
return;
}
// many calendar implementations put the date in aria-label, e.g. "November 13, 2025"
const aria = btn.getAttribute("aria-label") || btn.textContent || "";
// try to parse date from aria-label: new Date(aria) works for many locales
const parsed = new Date(aria);
if (isNaN(parsed.getTime())) {
// sometimes aria-label is like "13" (just day) - try data-day attribute
const dataDay = btn.getAttribute("data-day");
if (dataDay) {
// try parse yyyy-mm-dd
const pd = new Date(dataDay);
if (!isNaN(pd.getTime())) {
const key = format(pd, "yyyy-MM-dd");
const count = availabilityCounts[key] ?? 0;
setTooltip({
x: ev.pageX + 10,
y: ev.pageY + 10,
text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`,
});
return;
}
}
setTooltip(null);
return;
}
// parsed is valid - convert to yyyy-MM-dd
const key = format(getBrazilDate(parsed), "yyyy-MM-dd");
const count = availabilityCounts[key] ?? 0;
setTooltip({
x: ev.pageX + 10,
y: ev.pageY + 10,
text: `${count} horário${count !== 1 ? "s" : ""} disponíveis`,
});
};
const onLeave = () => setTooltip(null);
cont.addEventListener("mousemove", onMove);
cont.addEventListener("mouseleave", onLeave);
return () => {
cont.removeEventListener("mousemove", onMove);
cont.removeEventListener("mouseleave", onLeave);
};
}, [availabilityCounts]);
return ( return (
<PatientLayout> <PatientLayout>
<div className="space-y-6"> <div className="max-w-6xl mx-auto space-y-4 px-4">
<h1 className="text-2xl font-bold">Agendar Consulta</h1> <h1 className="text-2xl font-semibold">Agendar Consulta</h1>
<Card> <Card className="border rounded-xl shadow-sm">
<CardHeader> <CardHeader>
<CardTitle>Dados da Consulta</CardTitle> <CardTitle>Dados da Consulta</CardTitle>
<CardDescription>Escolha o médico, data e horário</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-2 gap-6 w-full">
{/* Médico */} {/* LEFT */}
<div className="space-y-2"> <div className="space-y-3">
<Label>Médico</Label>
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
<SelectTrigger>
<SelectValue placeholder="Selecione o médico" />
</SelectTrigger>
<SelectContent>
{loadingDoctors ? (
<SelectItem value="loading" disabled>
Carregando...
</SelectItem>
) : (
doctors.map((d: Doctor) => (
<SelectItem key={d.id} value={d.id}>
{d.full_name} - {d.specialty}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{/* Data */}
<div className="space-y-2">
<Label>Data</Label>
<Input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
min={new Date().toISOString().split("T")[0]}
disabled={!selectedDoctor}
/>
</div>
{/* Horário */}
<div className="space-y-2">
<Label>Horário</Label>
<Select
value={selectedTime}
onValueChange={setSelectedTime}
disabled={loadingSlots || availableTimes.length === 0}
>
<SelectTrigger>
<SelectValue
placeholder={
loadingSlots
? "Carregando horários..."
: availableTimes.length === 0
? "Nenhum horário disponível"
: "Selecione o horário"
}
/>
</SelectTrigger>
<SelectContent>
{availableTimes.map((h) => (
<SelectItem key={h} value={h}>
{h}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Tipo e duração */}
<div className="grid grid-cols-2 gap-4">
<div> <div>
<Label>Tipo</Label> <Label className="text-sm">Médico</Label>
<Select value={tipoConsulta} onValueChange={setTipoConsulta}> <Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue placeholder="Selecione o médico">
{selectedDoctor && doctors.find(d => d.id === selectedDoctor)?.full_name}
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="presencial">Presencial</SelectItem> {loadingDoctors ? (
<SelectItem value="online">Online</SelectItem> <SelectItem value="loading" disabled>Carregando...</SelectItem>
) : (
doctors.map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.full_name} {d.specialty}
</SelectItem>
))
)}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> <div>
<Label>Duração (min)</Label> <Label className="text-sm">Data</Label>
<Input <div ref={calendarRef} className="rounded-lg border p-2">
type="number" <CalendarShadcn
value={duracao} mode="single"
onChange={(e) => setDuracao(e.target.value)} disabled={!selectedDoctor}
min={10} selected={selectedDate ? new Date(selectedDate + "T12:00:00") : undefined}
max={120} onSelect={(date) => {
if (!date) return;
const fixedDate = new Date(date.getTime() + 12 * 60 * 60 * 1000);
const formatted = format(fixedDate, "yyyy-MM-dd");
setSelectedDate(formatted);
}}
className="rounded-md border shadow-sm p-2"
modifiers={{ selected: selectedDate ? new Date(selectedDate + 'T12:00:00') : undefined }}
modifiersClassNames={{
selected:
"bg-blue-600 text-white hover:bg-blue-700 rounded-md",
}}
/>
</div>
</div>
<div>
<Label className="text-sm">Observações</Label>
<Textarea
placeholder="Instruções para o médico..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="mt-2"
/> />
</div> </div>
</div> </div>
{/* Observações */} {/* RIGHT */}
<div className="space-y-2"> <div className="space-y-3">
<Label>Observações</Label> <Card className="shadow-md rounded-xl bg-blue-50 border border-blue-200">
<Textarea value={notes} onChange={(e) => setNotes(e.target.value)} /> <CardHeader>
</div> <CardTitle className="text-blue-700">Resumo</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-gray-900 text-sm">
<Button <div className="flex items-center justify-between">
type="submit" <div className="flex items-center gap-2">
disabled={!selectedDoctor || !selectedDate || !selectedTime} <User className="h-4 w-4 text-blue-600" />
className="w-full" <div className="text-xs">
> {selectedDoctor ? (
Agendar <div className="font-medium">
</Button> {doctors.find((d) => d.id === selectedDoctor)?.full_name}
</div>
) : (
<div className="text-gray-500">Médico</div>
)}
<div className="text-xs text-gray-500">
{selectedDoctor ? doctors.find(d => d.id === selectedDoctor)?.specialty : ""}
</div>
</div>
</div>
<div className="text-xs text-gray-600">{tipoConsulta} {duracao} min</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-blue-600" />
<div>
{selectedDate ? (
<div className="font-medium">{selectedDate.split("-").reverse().join("/")}</div>
) : (
<div className="text-gray-500">Data</div>
)}
</div>
</div>
<div className="text-xs text-gray-600"></div>
</div>
{/* Horário */}
<div className="space-y-2">
<Label>Horário</Label>
<Select onValueChange={setSelectedTime} disabled={
loadingSlots || availableTimes.length === 0
} >
<SelectTrigger> <SelectValue placeholder={
loadingSlots ? "Carregando horários..." : availableTimes.length === 0 ? "Nenhum horário disponível" : "Selecione o horário"
} />
</SelectTrigger>
<SelectContent> {
availableTimes.map((h) => ( <SelectItem key={h} value={h}> {h} </SelectItem> ))
} </SelectContent>
</Select>
</div>
{notes && (
<div className="flex items-start gap-2 text-sm">
<StickyNote className="h-4 w-4" />
<div className="italic text-gray-700">{notes}</div>
</div>
)}
</CardContent>
</Card>
<div className="flex gap-2">
<Button
type="submit"
className="w-full md:w-auto px-4 py-1.5 text-sm bg-blue-600 text-white hover:bg-blue-700"
disabled={!selectedDoctor || !selectedDate || !selectedTime}
>
Agendar
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
setSelectedDoctor("");
setSelectedDate("");
setSelectedTime("");
setAvailableTimes([]);
setNotes("");
}}
className="px-3"
>
Limpar
</Button>
</div>
</div>
</form> </form>
</CardContent> </CardContent>
</Card> </Card>
{/* Resumo */} {/* Tooltip element */}
<Card> {tooltip && (
<CardHeader> <div
<CardTitle className="flex items-center"> ref={tooltipRef}
<Calendar className="mr-2 h-5 w-5" /> Resumo style={{
</CardTitle> position: "absolute",
</CardHeader> left: tooltip.x,
<CardContent> top: tooltip.y,
{selectedDoctor && ( zIndex: 60,
<p> background: "rgba(0,0,0,0.85)",
<User className="inline-block w-4 h-4 mr-1" /> color: "white",
{doctors.find((d) => d.id === selectedDoctor)?.full_name} padding: "6px 8px",
</p> borderRadius: 6,
)} fontSize: 12,
{selectedDate && ( }}
<p> >
<Calendar className="inline-block w-4 h-4 mr-1" /> {tooltip.text}
{new Date(selectedDate).toLocaleDateString("pt-BR")} </div>
</p> )}
)}
{selectedTime && (
<p>
<Clock className="inline-block w-4 h-4 mr-1" />
{selectedTime}
</p>
)}
</CardContent>
</Card>
</div> </div>
</PatientLayout> </PatientLayout>
); );

View File

@ -61,7 +61,7 @@ export function LoginForm({ children }: LoginFormProps) {
case "secretary": case "secretary":
redirectPath = "/secretary/pacientes"; redirectPath = "/secretary/pacientes";
break; break;
case "paciente": case "patient":
redirectPath = "/patient/dashboard"; redirectPath = "/patient/dashboard";
break; break;
case "finance": case "finance":

View File

@ -44,6 +44,7 @@ interface DoctorData {
specialty: string; specialty: string;
department: string; department: string;
permissions: object; permissions: object;
role: string
} }
interface PatientLayoutProps { interface PatientLayoutProps {
@ -79,6 +80,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
crm: "", crm: "",
department: "", department: "",
permissions: {}, permissions: {},
role: userInfo.role
}); });
} else { } else {
// Se não encontrar, aí sim redireciona. // Se não encontrar, aí sim redireciona.
@ -292,6 +294,7 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
.split(" ") .split(" ")
.map((n) => n[0]) .map((n) => n[0])
.join("")} .join("")}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">

View File

@ -33,9 +33,11 @@ export async function login(email, senha) {
const data = await res.json(); const data = await res.json();
console.log("✅ Login bem-sucedido:", data); console.log("✅ Login bem-sucedido:", data);
if (typeof window !== "undefined" && data.access_token) { if (typeof window !== "undefined" && data.access_token) {
localStorage.setItem("token", data.access_token); localStorage.setItem("token", data.access_token);
} localStorage.setItem("user_info", JSON.stringify(data.user));
}
return data; return data;
} }

View File

@ -1,5 +1,8 @@
import { api } from "./api.mjs"; import { api } from "./api.mjs";
export const appointmentsService = { export const appointmentsService = {
/** /**
* Busca por horários disponíveis para agendamento. * Busca por horários disponíveis para agendamento.