diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 2033ca6..cf9bad5 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -4,12 +4,16 @@ import DoctorLayout from "@/components/doctor-layout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Calendar, Clock, User, Trash2 } from "lucide-react"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; + import Link from "next/link"; import { useEffect, useState } from "react"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { toast } from "@/hooks/use-toast"; + import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { exceptionsService } from "@/services/exceptionApi.mjs"; -import { toast } from "@/hooks/use-toast"; +import { doctorsService } from "@/services/doctorsApi.mjs"; +import { usersService } from "@/services/usersApi.mjs"; type Availability = { id: string; @@ -30,15 +34,86 @@ type Schedule = { weekday: object; }; +type Doctor = { + id: string; + user_id: string | null; + crm: string; + crm_uf: string; + specialty: string; + full_name: string; + cpf: string; + email: string; + phone_mobile: string | null; + phone2: string | null; + cep: string | null; + street: string | null; + number: string | null; + complement: string | null; + neighborhood: string | null; + city: string | null; + state: string | null; + birth_date: string | null; + rg: string | null; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; + max_days_in_advance: number; + rating: number | null; +} + +interface UserPermissions { + isAdmin: boolean; + isManager: boolean; + isDoctor: boolean; + isSecretary: boolean; + isAdminOrManager: boolean; +} + +interface UserData { + user: { + id: string; + email: string; + email_confirmed_at: string | null; + created_at: string | null; + last_sign_in_at: string | null; + }; + profile: { + id: string; + full_name: string; + email: string; + phone: string; + avatar_url: string | null; + disabled: boolean; + created_at: string | null; + updated_at: string | null; + }; + roles: string[]; + permissions: UserPermissions; +} + +interface Exception { + id: string; // id da exceção + doctor_id: string; + date: string; // formato YYYY-MM-DD + start_time: string | null; // null = dia inteiro + end_time: string | null; // null = dia inteiro + kind: "bloqueio" | "disponibilidade"; // tipos conhecidos + reason: string | null; // pode ser null + created_at: string; // timestamp ISO + created_by: string; +} + export default function PatientDashboard() { - var userInfo; - const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; //userInfo.id; + const [loggedDoctor, setLoggedDoctor] = useState(); + const [userData, setUserData] = useState(); const [availability, setAvailability] = useState(null); - const [exceptions, setExceptions] = useState(null); + const [exceptions, setExceptions] = useState([]); const [schedule, setSchedule] = useState>({}); - const formatTime = (time: string) => time.split(":").slice(0, 2).join(":"); + const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? ""; const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [patientToDelete, setPatientToDelete] = useState(null); + const [exceptionToDelete, setExceptionToDelete] = useState(null); const [error, setError] = useState(null); // Mapa de tradução @@ -52,35 +127,54 @@ export default function PatientDashboard() { saturday: "Sábado", }; - useEffect(() => { - userInfo = JSON.parse(localStorage.getItem("user_info") || "{}") - const fetchData = async () => { - userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); - try { - // fetch para disponibilidade - const response = await AvailabilityService.list(); - const filteredResponse = response.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); - setAvailability(filteredResponse); - // fetch para exceções - const res = await exceptionsService.list(); - const filteredRes = res.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); - setExceptions(filteredRes); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; - fetchData(); - }, []); + useEffect(() => { + const fetchData = async () => { + try { + const doctorsList: Doctor[] = await doctorsService.list(); + const doctor = doctorsList[0]; - const openDeleteDialog = (patientId: string) => { - setPatientToDelete(patientId); + // Salva no estado + setLoggedDoctor(doctor); + + // Busca disponibilidade + const availabilityList = await AvailabilityService.list(); + + // Filtra já com a variável local + const filteredAvail = availabilityList.filter( + (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id + ); + setAvailability(filteredAvail); + + // Busca exceções + const exceptionsList = await exceptionsService.list(); + const filteredExc = exceptionsList.filter( + (exc: { doctor_id: string }) => exc.doctor_id === doctor?.id + ); + console.log(exceptionsList) + setExceptions(filteredExc); + + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + + fetchData(); +}, []); + + // Função auxiliar para filtrar o id do doctor correspondente ao user logado + function findDoctorById(id: string, doctors: Doctor[]) { + return doctors.find((doctor) => doctor.user_id === id); + } + + const openDeleteDialog = (exceptionId: string) => { + setExceptionToDelete(exceptionId); setDeleteDialogOpen(true); }; - const handleDeletePatient = async (patientId: string) => { - // Remove from current list (client-side deletion) + const handleDeleteException = async (ExceptionId: string) => { try { - const res = await exceptionsService.delete(patientId); + alert(ExceptionId) + const res = await exceptionsService.delete(ExceptionId); let message = "Exceção deletada com sucesso"; try { @@ -96,7 +190,7 @@ export default function PatientDashboard() { description: message, }); - setExceptions((prev: any[]) => prev.filter((p) => String(p.id) !== String(patientId))); + setExceptions((prev: Exception[]) => prev.filter((p) => String(p.id) !== String(ExceptionId))); } catch (e: any) { toast({ title: "Erro", @@ -104,7 +198,7 @@ export default function PatientDashboard() { }); } setDeleteDialogOpen(false); - setPatientToDelete(null); + setExceptionToDelete(null); }; function formatAvailability(data: Availability[]) { @@ -258,12 +352,13 @@ export default function PatientDashboard() { {exceptions && exceptions.length > 0 ? ( - exceptions.map((ex: any) => { + exceptions.map((ex: Exception) => { // Formata data e hora const date = new Date(ex.date).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", month: "long", + timeZone: "UTC" }); const startTime = formatTime(ex.start_time); @@ -274,8 +369,10 @@ export default function PatientDashboard() {

{date}

-

- {startTime} - {endTime}
- +

+ {startTime && endTime + ? `${startTime} - ${endTime}` + : "Dia todo"}

@@ -301,11 +398,11 @@ export default function PatientDashboard() { Confirmar exclusão - Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. + Tem certeza que deseja excluir esta exceção? Esta ação não pode ser desfeita. Cancelar - patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + exceptionToDelete && handleDeleteException(exceptionToDelete)} className="bg-red-600 hover:bg-red-700"> Excluir diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx index 2669c40..3e7b316 100644 --- a/app/doctor/disponibilidade/excecoes/page.tsx +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -18,9 +18,36 @@ import { exceptionsService } from "@/services/exceptionApi.mjs"; // IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN import { Calendar } from "@/components/ui/calendar"; import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas -import { userInfo } from "os"; +import { doctorsService } from "@/services/doctorsApi.mjs"; -const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; +type Doctor = { + id: string; + user_id: string | null; + crm: string; + crm_uf: string; + specialty: string; + full_name: string; + cpf: string; + email: string; + phone_mobile: string | null; + phone2: string | null; + cep: string | null; + street: string | null; + number: string | null; + complement: string | null; + neighborhood: string | null; + city: string | null; + state: string | null; + birth_date: string | null; + rg: string | null; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; + max_days_in_advance: number; + rating: number | null; +} // --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE --- interface LocalStorageAppointment { @@ -35,8 +62,6 @@ interface LocalStorageAppointment { phone: string; } -const LOGGED_IN_DOCTOR_NAME = "Dr. João Santos"; - // Função auxiliar para comparar se duas datas (Date objects) são o mesmo dia const isSameDay = (date1: Date, date2: Date) => { return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); @@ -49,17 +74,24 @@ export default function ExceptionPage() { const router = useRouter(); const [filteredAppointments, setFilteredAppointments] = useState([]); const [isLoading, setIsLoading] = useState(false); - var userInfo; - const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; + const [loggedDoctor, setLoggedDoctor] = useState(); const [tipo, setTipo] = useState(""); - useEffect (()=>{ - userInfo = JSON.parse(localStorage.getItem("user_info") || "{}") - }) - useEffect(() => { - userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); - }); + const fetchData = async () => { + try { + const doctorsList: Doctor[] = await doctorsService.list(); + const doctor = doctorsList[0]; + + // Salva no estado + setLoggedDoctor(doctor); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + + fetchData(); + }, []); // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) const [bookedDays, setBookedDays] = useState([]); @@ -75,11 +107,11 @@ export default function ExceptionPage() { const formData = new FormData(form); const apiPayload = { - doctor_id: doctorIdTemp, - created_by: doctorIdTemp, + doctor_id: loggedDoctor?.id, + created_by: loggedDoctor?.user_id, date: selectedCalendarDate ? format(selectedCalendarDate, "yyyy-MM-dd") : "", - start_time: ((formData.get("horarioEntrada") + ":00") as string) || undefined, - end_time: ((formData.get("horarioSaida") + ":00") as string) || undefined, + start_time: ((formData.get("horarioEntrada")?formData.get("horarioEntrada") + ":00":null) as string) || null, + end_time: ((formData.get("horarioSaida")?formData.get("horarioSaida") + ":00":null) as string) || null, kind: tipo || undefined, reason: formData.get("reason"), }; @@ -176,13 +208,13 @@ export default function ExceptionPage() { - +
- +
@@ -196,7 +228,7 @@ export default function ExceptionPage() { Bloqueio - Liberação + Disponibilidade extra diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index dc304cc..0e2cf70 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -2,15 +2,24 @@ import { useState, useEffect } from "react"; import Link from "next/link"; + 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 DoctorLayout from "@/components/doctor-layout"; + import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { usersService } from "@/services/usersApi.mjs"; +import { doctorsService } from "@/services/doctorsApi.mjs"; + import { toast } from "@/hooks/use-toast"; import { useRouter } from "next/navigation"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; +import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; interface UserPermissions { isAdmin: boolean; @@ -42,29 +51,195 @@ interface UserData { permissions: UserPermissions; } +type Doctor = { + id: string; + user_id: string | null; + crm: string; + crm_uf: string; + specialty: string; + full_name: string; + cpf: string; + email: string; + phone_mobile: string | null; + phone2: string | null; + cep: string | null; + street: string | null; + number: string | null; + complement: string | null; + neighborhood: string | null; + city: string | null; + state: string | null; + birth_date: string | null; + rg: string | null; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; + max_days_in_advance: number; + rating: number | null; +} + +type Availability = { + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; + slot_minutes: number; + appointment_type: string; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; +}; + export default function AvailabilityPage() { const [error, setError] = useState(null); const router = useRouter(); const [isLoading, setIsLoading] = useState(false); + const [schedule, setSchedule] = useState>({}); + const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? ""; const [userData, setUserData] = useState(); + const [availability, setAvailability] = useState(null); + const [doctorId, setDoctorId] = useState(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [modalidadeConsulta, setModalidadeConsulta] = useState(""); + const [selectedAvailability, setSelectedAvailability] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + + const selectAvailability = (schedule: { start: string; end: string;}, day: string) => { + const selected = availability.filter((a: Availability) => + a.start_time === schedule.start && + a.end_time === schedule.end && + a.weekday === day + ); + setSelectedAvailability(selected[0]); + } - useEffect(() => { - const fetchData = async () => { + const handleOpenModal = (schedule: { start: string; end: string;}, day: string) => { + selectAvailability(schedule, day) + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setSelectedAvailability(null); + setIsModalOpen(false); + }; + + const handleEdit = async (formData:{ start_time: "", end_time: "", slot_minutes: "", appointment_type: "", id:""}) => { + if (isLoading) return; + setIsLoading(true); + + const apiPayload = { + start_time: formData.start_time, + end_time: formData.end_time, + slot_minutes: formData.slot_minutes, + appointment_type: formData.appointment_type, + }; + console.log(apiPayload); + + try { + const res = await AvailabilityService.update(formData.id, apiPayload); + console.log(res); + + let message = "disponibilidade editada com sucesso"; try { - const response = await AvailabilityService.list(); - console.log(response); - const user = await usersService.getMe(); - console.log(user); - setUserData(user); + if (!res[0].id) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + router.push("#") + } catch (err: any) { + toast({ + title: "Erro", + description: err?.message || "Não foi possível editar a disponibilidade", + }); + } finally { + setIsLoading(false); + handleCloseModal(); + fetchData() + } + }; + + // Mapa de tradução + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", + }; + const fetchData = async () => { + try { + const loggedUser = await usersService.getMe(); + const doctorList = await doctorsService.list(); + setUserData(loggedUser); + const doctor = findDoctorById(loggedUser.user.id, doctorList); + setDoctorId(doctor?.id); + console.log(doctor); + // Busca disponibilidade + const availabilityList = await AvailabilityService.list(); + + // Filtra já com a variável local + const filteredAvail = availabilityList.filter( + (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id + ); + setAvailability(filteredAvail); } catch (e: any) { alert(`${e?.error} ${e?.message}`); } }; + useEffect(() => { fetchData(); }, []); + // Função auxiliar para filtrar o id do doctor correspondente ao user logado + function findDoctorById(id: string, doctors: Doctor[]) { + return doctors.find((doctor) => doctor.user_id === id); + } + + + function formatAvailability(data: Availability[]) { + // Agrupar os horários por dia da semana + const schedule = data.reduce((acc: any, item) => { + const { weekday, start_time, end_time } = item; + + // Se o dia ainda não existe, cria o array + if (!acc[weekday]) { + acc[weekday] = []; + } + + // Adiciona o horário do dia + acc[weekday].push({ + start: start_time, + end: end_time, + }); + + return acc; + }, {} as Record); + + return schedule; + } + + useEffect(() => { + if (availability) { + const formatted = formatAvailability(availability); + setSchedule(formatted); + } + }, [availability]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (isLoading) return; @@ -73,7 +248,7 @@ export default function AvailabilityPage() { const formData = new FormData(form); const apiPayload = { - doctor_id: userData?.user.id, + doctor_id: doctorId, weekday: (formData.get("weekday") as string) || undefined, start_time: (formData.get("horarioEntrada") as string) || undefined, end_time: (formData.get("horarioSaida") as string) || undefined, @@ -104,13 +279,47 @@ export default function AvailabilityPage() { } catch (err: any) { toast({ title: "Erro", - description: err?.message || "Não foi possível cadastrar o paciente", + description: err?.message || "Não foi possível criar a disponibilidade", }); } finally { setIsLoading(false); } }; + const openDeleteDialog = (schedule: { start: string; end: string;}, day: string) => { + selectAvailability(schedule, day) + setDeleteDialogOpen(true); + }; + + const handleDeleteAvailability = async (AvailabilityId: string) => { + try { + const res = await AvailabilityService.delete(AvailabilityId); + + let message = "Disponibilidade deletada com sucesso"; + try { + if (res) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + + setAvailability((prev: Availability[]) => prev.filter((p) => String(p.id) !== String(AvailabilityId))); + } catch (e: any) { + toast({ + title: "Erro", + description: e?.message || "Não foi possível deletar a disponibilidade", + }); + } + setDeleteDialogOpen(false); + setSelectedAvailability(null); + }; + return (
@@ -152,7 +361,7 @@ export default function AvailabilityPage() {
-
+
- + - - - - +
+ + + + +
+
+ + + Horário Semanal + Confira ou altere a sua disponibilidade da semana + + + {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { + const times = schedule[day] || []; + return ( +
+
+
+

{weekdaysPT[day]}

+
+
+ {times.length > 0 ? ( + times.map((t, i) => ( +
+ + +

+ {formatTime(t.start)}
{formatTime(t.end)} +

+
+ + handleOpenModal(t, day)}> + + Editar + + openDeleteDialog(t, day)} + className="text-red-600"> + + Excluir + + +
+
+ )) + ) : ( +

Sem horário

+ )} +
+
+
+ ); + })} +
+
+
+ + + + Confirmar exclusão + Tem certeza que deseja excluir esta disponibilidade? Esta ação não pode ser desfeita. + + + Cancelar + selectedAvailability && handleDeleteAvailability(selectedAvailability.id)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + +
+ +
); } diff --git a/components/ui/availability-edit-modal.tsx b/components/ui/availability-edit-modal.tsx new file mode 100644 index 0000000..62b6e78 --- /dev/null +++ b/components/ui/availability-edit-modal.tsx @@ -0,0 +1,130 @@ +'use client' + +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useEffect, useState } from "react"; +import { start } from "repl"; +import { appointmentsService } from "@/services/appointmentsApi.mjs"; + +type Availability = { + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; + slot_minutes: number; + appointment_type: string; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; +}; + +interface AvailabilityEditModalProps { + isOpen: boolean; + availability: Availability | null; + onClose: () => void; + onSubmit: (formData: any) => void; +} + +export function AvailabilityEditModal({ availability, isOpen, onClose, onSubmit }: AvailabilityEditModalProps) { + const [modalidadeConsulta, setModalidadeConsulta] = useState(""); + const [form, setForm] = useState({ start_time: "", end_time: "", slot_minutes: "", appointment_type: "", id:availability?.id}); + // Mapa de tradução + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda-Feira", + tuesday: "Terça-Feira", + wednesday: "Quarta-Feira", + thursday: "Quinta-Feira", + friday: "Sexta-Feira", + saturday: "Sábado", + }; + + const handleInputChange = (field: string, value: string) => { + setForm((prev) => ({ ...prev, [field]: value })); + }; + + const handleFormSubmit = () => { + onSubmit(form); + }; + + useEffect(() => { + if (availability) { + setModalidadeConsulta(availability.appointment_type); + setForm({ + start_time: availability.start_time, + end_time: availability.end_time, + slot_minutes: availability.slot_minutes.toString(), + appointment_type: availability.appointment_type, + id: availability.id + }); + } + }, [availability]) + + if (!availability) { + return null; +} + + return ( + + + + Edite a disponibilidade + Altere a disponibilidade atual. + +
{ e.preventDefault(); handleFormSubmit(); }}> +
+

{weekdaysPT[availability.weekday]}

+
+
+ + handleInputChange("start_time", e.target.value)}/> +
+
+ + handleInputChange("end_time", e.target.value)}/> +
+
+
+
+ + handleInputChange("slot_minutes", e.target.value)} name="duracaoConsulta" required className="mt-1" /> +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ ); +}