368 lines
15 KiB
TypeScript
368 lines
15 KiB
TypeScript
// Caminho: app/(patient)/schedule/page.tsx (Completo e Corrigido)
|
|
"use client";
|
|
|
|
import type React from "react";
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { format, getDay } from "date-fns";
|
|
import { ptBR } from "date-fns/locale";
|
|
// A importação do PatientLayout foi REMOVIDA
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { Calendar as CalendarIcon, Clock, User as UserIcon } from "lucide-react";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import { toast } from "sonner";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { usuariosApi, User } from "@/services/usuariosApi";
|
|
import { medicosApi, Doctor } from "@/services/medicosApi";
|
|
import { agendamentosApi } from "@/services/agendamentosApi";
|
|
import { disponibilidadeApi, DoctorAvailability, DoctorException } from "@/services/disponibilidadeApi";
|
|
|
|
interface AvailabilityRules {
|
|
weekly: DoctorAvailability[];
|
|
exceptions: DoctorException[];
|
|
}
|
|
|
|
export default function ScheduleAppointment() {
|
|
const router = useRouter();
|
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
|
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
|
|
|
|
const [availabilityRules, setAvailabilityRules] = useState<AvailabilityRules | null>(null);
|
|
const [isAvailabilityLoading, setIsAvailabilityLoading] = useState(false);
|
|
|
|
const [formData, setFormData] = useState<{
|
|
doctorId: string;
|
|
date: Date | undefined;
|
|
time: string;
|
|
appointmentType: string;
|
|
duration: string;
|
|
reason: string;
|
|
}>({
|
|
doctorId: "",
|
|
date: undefined,
|
|
time: "",
|
|
appointmentType: "presencial",
|
|
duration: "30",
|
|
reason: "",
|
|
});
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSlotsLoading, setIsSlotsLoading] = useState(false);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const loadInitialData = async () => {
|
|
try {
|
|
const currentUser = await usuariosApi.getCurrentUser();
|
|
setUser(currentUser);
|
|
|
|
let activeDoctors = await medicosApi.list({ active: true });
|
|
|
|
if (activeDoctors.length === 0) {
|
|
console.warn("Nenhum médico ativo encontrado. Buscando do mock...");
|
|
toast.info("Usando dados de exemplo para a lista de médicos.");
|
|
activeDoctors = await medicosApi.getMockDoctors();
|
|
}
|
|
|
|
setDoctors(activeDoctors);
|
|
} catch (e) {
|
|
console.error("Erro ao carregar dados iniciais:", e);
|
|
setError("Não foi possível carregar os dados necessários para o agendamento.");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
loadInitialData();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const fetchDoctorAvailability = async () => {
|
|
if (!formData.doctorId) {
|
|
setAvailabilityRules(null);
|
|
return;
|
|
}
|
|
|
|
if (formData.doctorId.startsWith("mock-")) {
|
|
setAvailabilityRules({ weekly: [], exceptions: [] });
|
|
return;
|
|
}
|
|
|
|
setIsAvailabilityLoading(true);
|
|
try {
|
|
const [weekly, exceptions] = await Promise.all([
|
|
disponibilidadeApi.list({ doctor_id: formData.doctorId, active: true }),
|
|
disponibilidadeApi.listExceptions({ doctor_id: formData.doctorId }),
|
|
]);
|
|
setAvailabilityRules({ weekly, exceptions });
|
|
} catch (err) {
|
|
console.error("Erro ao buscar disponibilidade do médico:", err);
|
|
toast.error("Não foi possível carregar a agenda do médico.");
|
|
setAvailabilityRules(null);
|
|
} finally {
|
|
setIsAvailabilityLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDoctorAvailability();
|
|
}, [formData.doctorId]);
|
|
|
|
const fetchAvailableSlots = (doctorId: string, date: Date | undefined) => {
|
|
if (!doctorId || !date) return;
|
|
|
|
setIsSlotsLoading(true);
|
|
setAvailableSlots([]);
|
|
|
|
if (doctorId.startsWith("mock-")) {
|
|
setTimeout(() => {
|
|
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
|
|
setAvailableSlots(mockSlots);
|
|
setIsSlotsLoading(false);
|
|
}, 500);
|
|
return;
|
|
}
|
|
|
|
const formattedDate = format(date, "yyyy-MM-dd");
|
|
|
|
agendamentosApi.getAvailableSlots(doctorId, formattedDate)
|
|
.then(response => {
|
|
const slots = response.slots.filter(s => s.available).map(s => s.time);
|
|
setAvailableSlots(slots);
|
|
})
|
|
.catch(() => toast.error("Não foi possível buscar horários para esta data."))
|
|
.finally(() => setIsSlotsLoading(false));
|
|
};
|
|
|
|
const handleSelectChange = (name: keyof typeof formData) => (value: string | Date | undefined) => {
|
|
const newFormData = { ...formData, [name]: value } as any;
|
|
if (name === 'doctorId') {
|
|
newFormData.date = undefined;
|
|
newFormData.time = "";
|
|
}
|
|
if (name === 'date') {
|
|
newFormData.time = "";
|
|
fetchAvailableSlots(newFormData.doctorId, newFormData.date);
|
|
}
|
|
setFormData(newFormData);
|
|
};
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
const { id, value } = e.target;
|
|
setFormData(prev => ({ ...prev, [id]: value }));
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!user?.id || !formData.date) {
|
|
toast.error("Erro de autenticação ou data inválida.");
|
|
return;
|
|
}
|
|
|
|
if (formData.doctorId.startsWith("mock-")) {
|
|
toast.success("Simulação de agendamento com médico de exemplo concluída!");
|
|
router.push("/patient/appointments");
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
const newScheduledAt = new Date(`${format(formData.date, "yyyy-MM-dd")}T${formData.time}:00Z`).toISOString();
|
|
|
|
await agendamentosApi.create({
|
|
doctor_id: formData.doctorId,
|
|
patient_id: user.id,
|
|
scheduled_at: newScheduledAt,
|
|
duration_minutes: parseInt(formData.duration, 10),
|
|
appointment_type: formData.appointmentType as 'presencial' | 'telemedicina',
|
|
status: "requested",
|
|
created_by: user.id,
|
|
notes: formData.reason,
|
|
});
|
|
|
|
toast.success("Consulta agendada com sucesso!");
|
|
router.push("/patient/appointments");
|
|
} catch (error) {
|
|
console.error("Erro ao agendar consulta:", error);
|
|
toast.error("Falha ao agendar a consulta. Tente novamente.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const isDateDisabled = (date: Date): boolean => {
|
|
if (date < new Date(new Date().setDate(new Date().getDate() - 1))) {
|
|
return true;
|
|
}
|
|
|
|
if (!availabilityRules) {
|
|
return false;
|
|
}
|
|
|
|
const dateString = format(date, "yyyy-MM-dd");
|
|
const dayOfWeek = getDay(date);
|
|
|
|
const fullDayBlock = availabilityRules.exceptions.find(
|
|
ex => ex.date === dateString && ex.kind === 'bloqueio' && !ex.start_time
|
|
);
|
|
if (fullDayBlock) {
|
|
return true;
|
|
}
|
|
|
|
const worksOnThisDay = availabilityRules.weekly.some(
|
|
avail => avail.weekday === dayOfWeek
|
|
);
|
|
|
|
return !worksOnThisDay;
|
|
};
|
|
|
|
const selectedDoctorDetails = doctors.find((d) => d.id === formData.doctorId);
|
|
const isFormInvalid = !formData.doctorId || !formData.date || !formData.time || isSubmitting;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Agendar Consulta</h1>
|
|
<p className="text-muted-foreground">Escolha o médico, data e horário para sua consulta</p>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<p className="text-muted-foreground">Carregando...</p>
|
|
) : error ? (
|
|
<p className="text-destructive">{error}</p>
|
|
) : (
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
|
<div className="lg:col-span-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Dados da Consulta</CardTitle>
|
|
<CardDescription>Preencha as informações para agendar sua consulta</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="doctorId">Médico</Label>
|
|
<Select value={formData.doctorId} onValueChange={handleSelectChange('doctorId')}>
|
|
<SelectTrigger><SelectValue placeholder="Selecione um médico" /></SelectTrigger>
|
|
<SelectContent>
|
|
{doctors.length > 0 ? (
|
|
doctors.map((doctor) => (
|
|
<SelectItem key={doctor.id} value={doctor.id}>
|
|
{doctor.full_name} - {doctor.specialty}
|
|
</SelectItem>
|
|
))
|
|
) : (
|
|
<div className="p-2 text-center text-sm text-muted-foreground">Nenhum médico disponível</div>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="date">Data</Label>
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant={"outline"}
|
|
className={cn("w-full justify-start text-left font-normal", !formData.date && "text-muted-foreground")}
|
|
disabled={!formData.doctorId || isAvailabilityLoading}
|
|
>
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{isAvailabilityLoading ? "Carregando agenda..." : formData.date ? format(formData.date, "PPP", { locale: ptBR }) : <span>Escolha uma data</span>}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto p-0">
|
|
<Calendar
|
|
mode="single"
|
|
selected={formData.date}
|
|
onSelect={handleSelectChange('date')}
|
|
initialFocus
|
|
disabled={isDateDisabled}
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="time">Horário</Label>
|
|
<Select value={formData.time} onValueChange={handleSelectChange('time')} disabled={!formData.date || isSlotsLoading}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={isSlotsLoading ? "Carregando..." : "Selecione um horário"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{isSlotsLoading ? (
|
|
<div className="p-2 text-center text-sm text-muted-foreground">Carregando horários...</div>
|
|
) : availableSlots.length > 0 ? (
|
|
availableSlots.map((time) => <SelectItem key={time} value={time}>{time}</SelectItem>)
|
|
) : (
|
|
<div className="p-2 text-center text-sm text-muted-foreground">
|
|
{formData.date ? "Nenhum horário disponível" : "Selecione uma data"}
|
|
</div>
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="appointmentType">Tipo de Consulta</Label>
|
|
<Select value={formData.appointmentType} onValueChange={handleSelectChange('appointmentType')}>
|
|
<SelectTrigger id="appointmentType"><SelectValue placeholder="Selecione o tipo" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="presencial">Presencial</SelectItem>
|
|
<SelectItem value="telemedicina">Telemedicina</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="duration">Duração (minutos)</Label>
|
|
<Input id="duration" type="number" min={10} max={120} value={formData.duration} onChange={handleInputChange} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="reason">Queixa Principal / Observações (opcional)</Label>
|
|
<Textarea id="reason" placeholder="Descreva brevemente o motivo da consulta ou observações importantes..." value={formData.reason} onChange={handleInputChange} rows={3} />
|
|
</div>
|
|
|
|
<Button type="submit" className="w-full" disabled={isFormInvalid}>
|
|
{isSubmitting ? "Agendando..." : "Agendar Consulta"}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader><CardTitle className="flex items-center"><CalendarIcon className="mr-2 h-5 w-5" /> Resumo</CardTitle></CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{selectedDoctorDetails && <div className="flex items-center space-x-2"><UserIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{selectedDoctorDetails.full_name}</span></div>}
|
|
{formData.date && <div className="flex items-center space-x-2"><CalendarIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{format(formData.date, "PPP", { locale: ptBR })}</span></div>}
|
|
{formData.time && <div className="flex items-center space-x-2"><Clock className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{formData.time}</span></div>}
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader><CardTitle>Informações Importantes</CardTitle></CardHeader>
|
|
<CardContent className="text-sm text-muted-foreground space-y-2">
|
|
<p>• Chegue com 15 minutos de antecedência</p>
|
|
<p>• Traga documento com foto</p>
|
|
<p>• Traga carteirinha do convênio</p>
|
|
<p>• Traga exames anteriores, se houver</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |