diff --git a/app/doctor/consultas/page.tsx b/app/doctor/consultas/page.tsx index 9332a8b..8eca4d2 100644 --- a/app/doctor/consultas/page.tsx +++ b/app/doctor/consultas/page.tsx @@ -31,7 +31,7 @@ interface EnrichedAppointment { } export default function DoctorAppointmentsPage() { - const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: 'medico' }); + const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: "medico" }); const [allAppointments, setAllAppointments] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -111,13 +111,22 @@ export default function DoctorAppointmentsPage() { return format(date, "EEEE, dd 'de' MMMM", { locale: ptBR }); }; + const statusPT: Record = { + confirmed: "Confirmada", + completed: "Concluída", + cancelled: "Cancelada", + requested: "Solicitada", + no_show: "oculta", + checked_in: "Aguardando", + }; + const getStatusVariant = (status: EnrichedAppointment['status']) => { switch (status) { - case "confirmed": case "checked_in": return "default"; - case "completed": return "secondary"; - case "cancelled": case "no_show": return "destructive"; - case "requested": return "outline"; - default: return "outline"; + case "confirmed": case "checked_in": return "text-foreground bg-blue-100 hover:bg-blue-150"; + case "completed": return "text-foreground bg-green-100 hover:bg-green-150"; + case "cancelled": case "no_show": return "text-foreground bg-red-200 hover:bg-red-250"; + case "requested": return "text-foreground bg-yellow-100 hover:bg-yellow-150"; + default: return "border-gray bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"; } }; @@ -191,7 +200,7 @@ export default function DoctorAppointmentsPage() { {/* Coluna 2: Status e Telefone */}
- {appointment.status.replace('_', ' ')} + {statusPT[appointment.status].replace('_', ' ')}
{appointment.patientPhone} diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 83d41b2..aa382d4 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -29,6 +29,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs"; import { doctorsService } from "@/services/doctorsApi.mjs"; import { usersService } from "@/services/usersApi.mjs"; import Sidebar from "@/components/Sidebar"; +import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard"; type Availability = { id: string; @@ -165,20 +166,17 @@ export default function PatientDashboard() { ); 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}`); - } - }; + // Busca exceções + const exceptionsList = await exceptionsService.list(); + const filteredExc = exceptionsList.filter((exc: { doctor_id: string }) => exc.doctor_id === doctor?.id); + setExceptions(filteredExc); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; - fetchData(); - }, []); + fetchData(); + }, []); // Função auxiliar para filtrar o id do doctor correspondente ao user logado function findDoctorById(id: string, doctors: Doctor[]) { @@ -320,82 +318,42 @@ export default function PatientDashboard() { - - - Próximas Consultas - Suas consultas agendadas - - -
-
-
-

Dr. João Santos

-

Cardiologia

-
-
-

02 out

-

14:30

-
+ + + Próximas Consultas + Suas consultas agendadas + + +
+
+
+

Dr. João Santos

+

Cardiologia

+
+
+

02 out

+

14:30

+
+
+
+
+
-
-
-
-
-
- - - Horário Semanal - - Confira rapidamente 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)} -

- )) - ) : ( -

- Sem horário -

- )} -
-
-
- ); - })} -
-
-
-
- - - Exceções - - Bloqueios e liberações eventuais de agenda - - +
+ + + Horário Semanal + Confira rapidamente a sua disponibilidade da semana + + {loggedDoctor && } + +
+
+ + + Exceções + Bloqueios e liberações eventuais de agenda + {exceptions && exceptions.length > 0 ? ( @@ -411,75 +369,47 @@ export default function PatientDashboard() { const startTime = formatTime(ex.start_time); const endTime = formatTime(ex.end_time); - return ( -
-
-
-

{date}

-

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

-
-
-

- {ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"} -

-

- {ex.reason || "Sem motivo especificado"} -

-
-
- -
-
-
- ); - }) - ) : ( -

- Nenhuma exceção registrada. -

- )} -
-
-
- - - - Confirmar exclusão - - Tem certeza que deseja excluir esta exceção? Esta ação não pode - ser desfeita. - - - - Cancelar - - exceptionToDelete && handleDeleteException(exceptionToDelete) - } - className="bg-red-600 hover:bg-red-700" - > - Excluir - - - - -
- - ); + return ( +
+
+
+

{date}

+

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

+
+
+

{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"}

+

{ex.reason || "Sem motivo especificado"}

+
+
+ +
+
+
+ ); + }) + ) : ( +

Nenhuma exceção registrada.

+ )} + + +
+ + + + Confirmar exclusão + Tem certeza que deseja excluir esta exceção? Esta ação não pode ser desfeita. + + + Cancelar + exceptionToDelete && handleDeleteException(exceptionToDelete)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + + + + + ); } diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index c4947e4..7fd25fd 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -218,36 +218,36 @@ export default function AvailabilityPage() { } }; - // 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}`); - } - }; + // 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(); @@ -320,20 +320,21 @@ export default function AvailabilityPage() { } } catch {} - toast({ - title: "Sucesso", - description: message, - }); - router.push("#"); // adicionar página para listar a disponibilidade - } catch (err: any) { - toast({ - title: "Erro", - description: err?.message || "Não foi possível criar a disponibilidade", - }); - } finally { - setIsLoading(false); - } - }; + toast({ + title: "Sucesso", + description: message, + }); + router.push("#"); // adicionar página para listar a disponibilidade + } catch (err: any) { + toast({ + title: "Erro", + description: err?.message || "Não foi possível criar a disponibilidade", + }); + } finally { + fetchData() + setIsLoading(false); + } + }; const openDeleteDialog = ( schedule: { start: string; end: string }, @@ -343,38 +344,35 @@ export default function AvailabilityPage() { 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); - }; + 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", + }); + } + fetchData() + setDeleteDialogOpen(false); + setSelectedAvailability(null); + }; return ( @@ -542,142 +540,99 @@ export default function AvailabilityPage() { - {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} - {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} - {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */} -
- - - -
- {" "} - {/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */} - - - - -
-
- - - {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */} -
- - - Horário Semanal - - Confira ou altere a sua disponibilidade da semana - - - {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */} - - {[ - "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 focus:bg-red-50 focus:text-red-600" - > - - Excluir - - -
-
- )) - ) : ( -

- Sem horário -

- )} -
+ {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} + {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} + {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */} +
+ + + +
{/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */} + + + + +
-
- ); - })} - - -
- - {/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} - - - - 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 - - - - -
- -
- ); + + + {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */} +
+ + + Horário Semanal + Confira ou altere a sua disponibilidade da semana + + {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */} + + {["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 focus:bg-red-50 focus:text-red-600"> + + Excluir + + +
+
+ )) + ) : ( +

Sem horário

+ )} +
+
+
+ ); + })} +
+
+
+ + {/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} + + + + 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/app/manager/disponibilidade/page.tsx b/app/manager/disponibilidade/page.tsx new file mode 100644 index 0000000..adfcd80 --- /dev/null +++ b/app/manager/disponibilidade/page.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import Sidebar from "@/components/Sidebar"; +import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard"; + +import { useEffect, useState, useMemo } from "react"; + +import { AvailabilityService } from "@/services/availabilityApi.mjs"; +import { doctorsService } from "@/services/doctorsApi.mjs"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; +import { Filter } from "lucide-react"; + +type Doctor = { + id: string; + full_name: string; + specialty: string; + active: boolean; +}; + +type Availability = { + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; +}; + +export default function AllAvailabilities() { + const [availabilities, setAvailabilities] = useState(null); + const [doctors, setDoctors] = useState(null); + + // 🔎 Filtros + const [search, setSearch] = useState(""); + const [specialty, setSpecialty] = useState("all"); + + // 🔄 Paginação + const ITEMS_PER_PAGE = 6; + const [page, setPage] = useState(1); + + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const doctorsList = await doctorsService.list(); + setDoctors(doctorsList); + + const availabilityList = await AvailabilityService.list(); + setAvailabilities(availabilityList); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + // 🎯 Obter todas as especialidades existentes + const specialties = useMemo(() => { + if (!doctors) return []; + const unique = Array.from(new Set(doctors.map((d) => d.specialty))); + return unique; + }, [doctors]); + + // 🔍 Filtrar médicos por especialidade + nome + const filteredDoctors = useMemo(() => { + if (!doctors) return []; + + return doctors.filter((doctor) => (specialty === "all" ? true : doctor.specialty === specialty)).filter((doctor) => doctor.full_name.toLowerCase().includes(search.toLowerCase())); + }, [doctors, search, specialty]); + + // 📄 Paginação (após filtros!) + const totalPages = Math.ceil(filteredDoctors.length / ITEMS_PER_PAGE); + const paginatedDoctors = filteredDoctors.slice((page - 1) * ITEMS_PER_PAGE, page * ITEMS_PER_PAGE); + + const goNext = () => setPage((p) => Math.min(p + 1, totalPages)); + const goPrev = () => setPage((p) => Math.max(p - 1, 1)); + + if (loading) { + return ( + +
Carregando dados...
+
+ ); + } + + if (!doctors || !availabilities) { + return ( + +
Não foi possível carregar médicos ou disponibilidades.
+
+ ); + } + + return ( + +
+
+

Disponibilidade dos Médicos

+

Visualize a agenda semanal individual de cada médico.

+
+ + + {/* 🔎 Filtros */} +
+ {/* Filtro por nome */} + + { + setSearch(e.target.value); + setPage(1); + }} + className="w-full md:w-1/3" + /> + + {/* Filtro por especialidade */} + +
+
+
+ {/* GRID de cards */} +
+ {paginatedDoctors.map((doctor) => { + const doctorAvailabilities = availabilities.filter((a) => a.doctor_id === doctor.id); + + return ( + + + {doctor.full_name} + + + + + + + ); + })} +
+ + {/* 📄 Paginação */} + {totalPages > 1 && ( +
+ + + + Página {page} de {totalPages} + + + +
+ )} +
+
+ ); +} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index a43c9c1..aecf916 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -188,18 +188,14 @@ export default function Sidebar({ children }: SidebarProps) { }, ]; - const managerItems: MenuItem[] = [ - { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, - { href: "#", icon: ClipboardMinus, label: "Relatórios gerenciais" }, - { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, - { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, - { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, - { - href: "/secretary/appointments", - icon: CalendarCheck2, - label: "Consultas", - }, - ]; + const managerItems: MenuItem[] = [ + { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, + { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, + { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, + { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, + { href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" }, + { href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" }, + ]; switch (role) { case "gestor": diff --git a/components/ui/WeeklyScheduleCard.tsx b/components/ui/WeeklyScheduleCard.tsx new file mode 100644 index 0000000..2b33ba0 --- /dev/null +++ b/components/ui/WeeklyScheduleCard.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { AvailabilityService } from "@/services/availabilityApi.mjs"; +import { doctorsService } from "@/services/doctorsApi.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 WeeklyScheduleProps { + doctorId?: string; +} + +export default function WeeklyScheduleCard({ doctorId }: WeeklyScheduleProps) { + const [schedule, setSchedule] = useState>({}); + const [loading, setLoading] = useState(true); + + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", + }; + + const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? ""; + + function formatAvailability(data: Availability[]) { + const grouped = data.reduce((acc: any, item) => { + const { weekday, start_time, end_time } = item; + + if (!acc[weekday]) acc[weekday] = []; + + acc[weekday].push({ start: start_time, end: end_time }); + + return acc; + }, {}); + + return grouped; + } + + useEffect(() => { + const fetchSchedule = async () => { + try { + const availabilityList = await AvailabilityService.list(); + + const filtered = availabilityList.filter((a: Availability) => a.doctor_id == doctorId); + + const formatted = formatAvailability(filtered); + setSchedule(formatted); + } catch (err) { + console.error("Erro ao carregar horários:", err); + } finally { + setLoading(false); + } + }; + + fetchSchedule(); + }, []); + + return ( +
+ {loading ? ( +

Carregando...

+ ) : ( + ["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)} +

+ )) + ) : ( +

Sem horário

+ )} +
+
+
+ ); + }) + )} +
+ ); +}