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 060d8e4..aa382d4 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -1,9 +1,24 @@ "use client"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +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 { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import Link from "next/link"; import { useEffect, useState } from "react"; @@ -14,24 +29,25 @@ 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; - 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; + 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; }; type Schedule = { - weekday: object; + weekday: object; }; type Doctor = { @@ -61,36 +77,36 @@ type Doctor = { updated_by: string | null; max_days_in_advance: number; rating: number | null; -} +}; interface UserPermissions { - isAdmin: boolean; - isManager: boolean; - isDoctor: boolean; - isSecretary: boolean; - isAdminOrManager: boolean; + 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; + 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 { @@ -98,7 +114,7 @@ interface Exception { doctor_id: string; date: string; // formato YYYY-MM-DD start_time: string | null; // null = dia inteiro - end_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 @@ -106,188 +122,201 @@ interface Exception { } export default function PatientDashboard() { - const [loggedDoctor, setLoggedDoctor] = useState(); - const [userData, setUserData] = useState(); - const [availability, setAvailability] = useState(null); - const [exceptions, setExceptions] = useState([]); - const [schedule, setSchedule] = useState>({}); - const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? ""; - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [exceptionToDelete, setExceptionToDelete] = useState(null); - const [error, setError] = useState(null); + const [loggedDoctor, setLoggedDoctor] = useState(); + const [userData, setUserData] = useState(); + const [availability, setAvailability] = useState(null); + const [exceptions, setExceptions] = useState([]); + const [schedule, setSchedule] = useState< + Record + >({}); + const formatTime = (time?: string | null) => + time?.split(":")?.slice(0, 2).join(":") ?? ""; + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [exceptionToDelete, setExceptionToDelete] = useState( + null + ); + const [error, setError] = useState(null); - // Mapa de tradução - const weekdaysPT: Record = { - sunday: "Domingo", - monday: "Segunda", - tuesday: "Terça", - wednesday: "Quarta", - thursday: "Quinta", - friday: "Sexta", - saturday: "Sábado", - }; - - useEffect(() => { - const fetchData = async () => { - try { - const doctorsList: Doctor[] = await doctorsService.list(); - const doctor = doctorsList[0]; - - // 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}`); - } + // Mapa de tradução + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", }; - fetchData(); -}, []); + useEffect(() => { + const fetchData = async () => { + try { + const doctorsList: Doctor[] = await doctorsService.list(); + const doctor = doctorsList[0]; - // 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); - }; + // Salva no estado + setLoggedDoctor(doctor); - const handleDeleteException = async (ExceptionId: string) => { - try { - alert(ExceptionId) - const res = await exceptionsService.delete(ExceptionId); + // Busca disponibilidade + const availabilityList = await AvailabilityService.list(); - let message = "Exceção deletada com sucesso"; - try { - if (res) { - throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); - } else { - console.log(message); - } - } catch {} + // Filtra já com a variável local + const filteredAvail = availabilityList.filter( + (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id + ); + setAvailability(filteredAvail); - toast({ - title: "Sucesso", - description: message, - }); - - setExceptions((prev: Exception[]) => prev.filter((p) => String(p.id) !== String(ExceptionId))); - } catch (e: any) { - toast({ - title: "Erro", - description: e?.message || "Não foi possível deletar a exceção", - }); - } - setDeleteDialogOpen(false); - setExceptionToDelete(null); - }; - - 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] = []; + // 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}`); } + }; - // Adiciona o horário do dia - acc[weekday].push({ - start: start_time, - end: end_time, - }); + fetchData(); + }, []); - return acc; - }, {} as Record); + // 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); + } - return schedule; - } + const openDeleteDialog = (exceptionId: string) => { + setExceptionToDelete(exceptionId); + setDeleteDialogOpen(true); + }; - useEffect(() => { - if (availability) { - const formatted = formatAvailability(availability); - setSchedule(formatted); + const handleDeleteException = async (ExceptionId: string) => { + try { + alert(ExceptionId); + const res = await exceptionsService.delete(ExceptionId); + + let message = "Exceção deletada com sucesso"; + try { + if (res) { + throw new Error( + `${res.error} ${res.message}` || "A API retornou erro" + ); + } else { + console.log(message); } - }, [availability]); + } catch {} - return ( - -
-
-

Dashboard

-

Bem-vindo ao seu portal de consultas médicas

-
+ toast({ + title: "Sucesso", + description: message, + }); -
- - - Próxima Consulta - - - -
02 out
-

Dr. Silva - 14:30

-
-
+ setExceptions((prev: Exception[]) => + prev.filter((p) => String(p.id) !== String(ExceptionId)) + ); + } catch (e: any) { + toast({ + title: "Erro", + description: e?.message || "Não foi possível deletar a exceção", + }); + } + setDeleteDialogOpen(false); + setExceptionToDelete(null); + }; - - - Consultas Este Mês - - - -
4
-

4 agendadas

-
-
+ 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; - - - Perfil - - - -
100%
-

Dados completos

-
-
-
+ // Se o dia ainda não existe, cria o array + if (!acc[weekday]) { + acc[weekday] = []; + } -
- - - Ações Rápidas - Acesse rapidamente as principais funcionalidades - - - - - - - + // 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]); + + return ( + +
+
+

Dashboard

+

+ Bem-vindo ao seu portal de consultas médicas +

+
+ +
+ + + + Próxima Consulta + + + + +
02 out
+

Dr. Silva - 14:30

+
+
+ + + + + Consultas Este Mês + + + + +
4
+

4 agendadas

+
+
+ + + + Perfil + + + +
100%
+

Dados completos

+
+
+
+ +
+ + + Ações Rápidas + + Acesse rapidamente as principais funcionalidades + + + + + + + + @@ -316,31 +345,7 @@ export default function PatientDashboard() { 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

- )} -
-
-
- ); - })} -
+ {loggedDoctor && }
@@ -350,30 +355,26 @@ export default function PatientDashboard() { Bloqueios e liberações eventuais de agenda - - {exceptions && exceptions.length > 0 ? ( - 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" - }); + + {exceptions && exceptions.length > 0 ? ( + 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); - const endTime = formatTime(ex.end_time); + const startTime = formatTime(ex.start_time); + const endTime = formatTime(ex.end_time); return (

{date}

-

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

+

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

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

diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 80b7816..7fd25fd 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -6,7 +6,13 @@ 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { usersService } from "@/services/usersApi.mjs"; @@ -14,163 +20,203 @@ 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 { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Edit, 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"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import Sidebar from "@/components/Sidebar"; // ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas) interface UserPermissions { - isAdmin: boolean; - isManager: boolean; - isDoctor: boolean; - isSecretary: boolean; - isAdminOrManager: boolean; + 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; + 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; } 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; -} + 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; + 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]); - } + const [error, setError] = useState(null); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [schedule, setSchedule] = useState< + Record + >({}); + 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 handleOpenModal = (schedule: { start: string; end: string;}, day: string) => { - selectAvailability(schedule, day) - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setSelectedAvailability(null); - setIsModalOpen(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]); + }; + + 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); - const handleEdit = async (formData:{ start_time: "", end_time: "", slot_minutes: "", appointment_type: "", id:""}) => { - if (isLoading) return; - setIsLoading(true); + try { + const res = await AvailabilityService.update(formData.id, apiPayload); + console.log(res); - 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 { - 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() + let message = "disponibilidade editada com sucesso"; + try { + 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 = { @@ -183,95 +229,96 @@ export default function AvailabilityPage() { 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; - setIsLoading(true); - const form = e.currentTarget; - const formData = new FormData(form); - - const apiPayload = { - 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, - slot_minutes: Number(formData.get("duracaoConsulta")) || undefined, - appointment_type: modalidadeConsulta || undefined, - active: true, - }; - console.log(apiPayload); - try { - const res = await AvailabilityService.create(apiPayload); - console.log(res); + 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}`); + } + }; - let message = "disponibilidade cadastrada com sucesso"; - try { - if (!res[0].id) { - throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); - } else { - console.log(message); - } - } catch {} + 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; + setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + + const apiPayload = { + 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, + slot_minutes: Number(formData.get("duracaoConsulta")) || undefined, + appointment_type: modalidadeConsulta || undefined, + active: true, + }; + console.log(apiPayload); + + try { + const res = await AvailabilityService.create(apiPayload); + console.log(res); + + let message = "disponibilidade cadastrada com sucesso"; + try { + if (!res[0].id) { + throw new Error( + `${res.error} ${res.message}` || "A API retornou erro" + ); + } else { + console.log(message); + } + } catch {} toast({ title: "Sucesso", @@ -284,14 +331,18 @@ export default function AvailabilityPage() { description: err?.message || "Não foi possível criar a disponibilidade", }); } finally { + fetchData() setIsLoading(false); } }; - const openDeleteDialog = (schedule: { start: string; end: string;}, day: string) => { - selectAvailability(schedule, day) - setDeleteDialogOpen(true); - }; + const openDeleteDialog = ( + schedule: { start: string; end: string }, + day: string + ) => { + selectAvailability(schedule, day); + setDeleteDialogOpen(true); + }; const handleDeleteAvailability = async (AvailabilityId: string) => { try { @@ -318,101 +369,176 @@ export default function AvailabilityPage() { description: e?.message || "Não foi possível deletar a disponibilidade", }); } + fetchData() setDeleteDialogOpen(false); setSelectedAvailability(null); }; - return ( - -
-
-
-

Definir Disponibilidade

-

Defina sua disponibilidade para consultas

-
+ return ( + +
+
+
+

+ Definir Disponibilidade +

+

+ Defina sua disponibilidade para consultas{" "} +

+
+
+ +
+
+

Dados

+ +
+ {/* **AJUSTE DE RESPONSIVIDADE: DIAS DA SEMANA** */} +
+ + {/* O antigo 'flex gap-4 mt-2 flex-nowrap' foi substituído por um grid responsivo: */} +
+ + + + + + +
+
- -
-

Dados

+ {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */} + {/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */} +
+
+ + +
+
+ + +
+
+ + +
+ {/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */} +
-
- {/* **AJUSTE DE RESPONSIVIDADE: DIAS DA SEMANA** */} -
- - {/* O antigo 'flex gap-4 mt-2 flex-nowrap' foi substituído por um grid responsivo: */} -
- - - - - - - -
-
- - {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */} - {/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */} -
-
- - -
-
- - -
-
- - -
- {/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */} -
- -
- - -
-
-
+
+ + +
+
+
{/* **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 */} @@ -453,7 +579,7 @@ export default function AvailabilityPage() {
-

+

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

@@ -509,4 +635,4 @@ export default function AvailabilityPage() { ); -} \ No newline at end of file +} diff --git a/app/doctor/medicos/page.tsx b/app/doctor/medicos/page.tsx index 4dd6c87..1502fab 100644 --- a/app/doctor/medicos/page.tsx +++ b/app/doctor/medicos/page.tsx @@ -2,414 +2,463 @@ import { useEffect, useState, useCallback } from "react"; import Link from "next/link"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Eye, Edit, Calendar, Trash2, Loader2 } from "lucide-react"; import { api } from "@/services/api.mjs"; import { PatientDetailsModal } from "@/components/ui/patient-details-modal"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import Sidebar from "@/components/Sidebar"; interface Paciente { - id: string; - nome: string; - telefone: string; - cidade: string; - estado: string; - ultimoAtendimento?: string; - proximoAtendimento?: string; - email?: string; - birth_date?: string; - cpf?: string; - blood_type?: string; - weight_kg?: number; - height_m?: number; - street?: string; - number?: string; - complement?: string; - neighborhood?: string; - cep?: string; + id: string; + nome: string; + telefone: string; + cidade: string; + estado: string; + ultimoAtendimento?: string; + proximoAtendimento?: string; + email?: string; + birth_date?: string; + cpf?: string; + blood_type?: string; + weight_kg?: number; + height_m?: number; + street?: string; + number?: string; + complement?: string; + neighborhood?: string; + cep?: string; } export default function PacientesPage() { - const [pacientes, setPacientes] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [selectedPatient, setSelectedPatient] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); + const [pacientes, setPacientes] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [selectedPatient, setSelectedPatient] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); - // --- Lógica de Paginação INÍCIO --- - const [itemsPerPage, setItemsPerPage] = useState(5); - const [currentPage, setCurrentPage] = useState(1); + // --- Lógica de Paginação INÍCIO --- + const [itemsPerPage, setItemsPerPage] = useState(5); + const [currentPage, setCurrentPage] = useState(1); - const totalPages = Math.ceil(pacientes.length / itemsPerPage); + const totalPages = Math.ceil(pacientes.length / itemsPerPage); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); - // Funções de Navegação - const goToPrevPage = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; + // Funções de Navegação + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; - const goToNextPage = () => { - setCurrentPage((prev) => Math.min(totalPages, prev + 1)); - }; + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; - // Lógica para gerar os números das páginas visíveis (máximo de 5) - const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { - const pages: number[] = []; - const maxVisiblePages = 5; - const halfRange = Math.floor(maxVisiblePages / 2); - let startPage = Math.max(1, currentPage - halfRange); - let endPage = Math.min(totalPages, currentPage + halfRange); + // Lógica para gerar os números das páginas visíveis (máximo de 5) + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); - if (endPage - startPage + 1 < maxVisiblePages) { - if (endPage === totalPages) { - startPage = Math.max(1, totalPages - maxVisiblePages + 1); - } - if (startPage === 1) { - endPage = Math.min(totalPages, maxVisiblePages); - } - } + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - return pages; - }; + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; - const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); - // Lógica para mudar itens por página, resetando para a página 1 - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(Number(value)); - setCurrentPage(1); - }; - // --- Lógica de Paginação FIM --- + // Lógica para mudar itens por página, resetando para a página 1 + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + // --- Lógica de Paginação FIM --- + const handleOpenModal = (patient: Paciente) => { + setSelectedPatient(patient); + setIsModalOpen(true); + }; - const handleOpenModal = (patient: Paciente) => { - setSelectedPatient(patient); - setIsModalOpen(true); - }; + const handleCloseModal = () => { + setSelectedPatient(null); + setIsModalOpen(false); + }; - const handleCloseModal = () => { - setSelectedPatient(null); - setIsModalOpen(false); - }; + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "N/A"; + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat("pt-BR").format(date); + } catch (e) { + return dateString; // Retorna o string original se o formato for inválido + } + }; - const formatDate = (dateString: string | null | undefined) => { - if (!dateString) return "N/A"; - try { - const date = new Date(dateString); - return new Intl.DateTimeFormat("pt-BR").format(date); - } catch (e) { - return dateString; // Retorna o string original se o formato for inválido - } - }; + const fetchPacientes = useCallback(async () => { + try { + setLoading(true); + setError(null); + const json = await api.get("/rest/v1/patients"); + const items = Array.isArray(json) + ? json + : Array.isArray(json?.data) + ? json.data + : []; - const fetchPacientes = useCallback(async () => { - try { - setLoading(true); - setError(null); - const json = await api.get("/rest/v1/patients"); - const items = Array.isArray(json) - ? json - : Array.isArray(json?.data) - ? json.data - : []; + const mapped: Paciente[] = items.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? "N/A", + cidade: p.city ?? "N/A", + estado: p.state ?? "N/A", + ultimoAtendimento: formatDate(p.created_at), + proximoAtendimento: "N/A", // Necessita de lógica de agendamento real + email: p.email ?? "N/A", + birth_date: p.birth_date ?? "N/A", + cpf: p.cpf ?? "N/A", + blood_type: p.blood_type ?? "N/A", + weight_kg: p.weight_kg ?? 0, + height_m: p.height_m ?? 0, + street: p.street ?? "N/A", + number: p.number ?? "N/A", + complement: p.complement ?? "N/A", + neighborhood: p.neighborhood ?? "N/A", + cep: p.cep ?? "N/A", + })); - const mapped: Paciente[] = items.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "—", - telefone: p.phone_mobile ?? "N/A", - cidade: p.city ?? "N/A", - estado: p.state ?? "N/A", - ultimoAtendimento: formatDate(p.created_at), - proximoAtendimento: "N/A", // Necessita de lógica de agendamento real - email: p.email ?? "N/A", - birth_date: p.birth_date ?? "N/A", - cpf: p.cpf ?? "N/A", - blood_type: p.blood_type ?? "N/A", - weight_kg: p.weight_kg ?? 0, - height_m: p.height_m ?? 0, - street: p.street ?? "N/A", - number: p.number ?? "N/A", - complement: p.complement ?? "N/A", - neighborhood: p.neighborhood ?? "N/A", - cep: p.cep ?? "N/A", - })); + setPacientes(mapped); + setCurrentPage(1); // Resetar a página ao carregar novos dados + } catch (e: any) { + console.error("Erro ao carregar pacientes:", e); + setError(e?.message || "Erro ao carregar pacientes"); + } finally { + setLoading(false); + } + }, []); - setPacientes(mapped); - setCurrentPage(1); // Resetar a página ao carregar novos dados - } catch (e: any) { - console.error("Erro ao carregar pacientes:", e); - setError(e?.message || "Erro ao carregar pacientes"); - } finally { - setLoading(false); - } - }, []); + useEffect(() => { + fetchPacientes(); + }, [fetchPacientes]); - useEffect(() => { - fetchPacientes(); - }, [fetchPacientes]); + return ( + +
+ {/* Cabeçalho */} +
+ {" "} + {/* Ajustado para flex-col em telas pequenas */} +
+

Pacientes

+

+ Lista de pacientes vinculados +

+
+ {/* Controles de filtro e novo paciente */} + {/* Alterado para que o Select e o Link ocupem a largura total em telas pequenas e fiquem lado a lado em telas maiores */} +
+ + + + +
+
- return ( - -
- {/* Cabeçalho */} -
{/* Ajustado para flex-col em telas pequenas */} -
-

Pacientes

-

- Lista de pacientes vinculados -

+
+ {/* Tabela para Telas Médias e Grandes */} +
+ {" "} + {/* Esconde em telas pequenas */} + + + + + + + + + + + + + + {loading ? ( + + + + ) : error ? ( + + + + ) : pacientes.length === 0 ? ( + + + + ) : ( + currentItems.map((p) => ( + + + + + + + + + + )) + )} + +
+ Nome + + Telefone + + Cidade + + Estado + + Último atendimento + + Próximo atendimento + + Ações +
+ + Carregando pacientes... +
{`Erro: ${error}`}
+ Nenhum paciente encontrado +
{p.nome} + {p.telefone} + + {p.cidade} + + {p.estado} + + {p.ultimoAtendimento} + + {p.proximoAtendimento} + + + + + + + handleOpenModal(p)} + > + + Ver detalhes + + + + + Laudos + + + + alert(`Agenda para paciente ID: ${p.id}`) + } + > + + Ver agenda + + { + const newPacientes = pacientes.filter( + (pac) => pac.id !== p.id + ); + setPacientes(newPacientes); + alert(`Paciente ID: ${p.id} excluído`); + }} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + + Excluir + + + +
+
+ + {/* Layout em Cards/Lista para Telas Pequenas */} +
+ {" "} + {/* Visível apenas em telas pequenas */} + {loading ? ( +
+ + Carregando pacientes... +
+ ) : error ? ( +
{`Erro: ${error}`}
+ ) : pacientes.length === 0 ? ( +
+ Nenhum paciente encontrado +
+ ) : ( + currentItems.map((p) => ( +
+
+ {" "} + {/* Adicionado padding à direita */} +
+ {" "} + {/* Aumentado a fonte e break-words para evitar corte do nome */} + {p.nome || "—"}
- {/* Controles de filtro e novo paciente */} - {/* Alterado para que o Select e o Link ocupem a largura total em telas pequenas e fiquem lado a lado em telas maiores */} -
- - - - -
+ + Ver agenda + + { + const newPacientes = pacientes.filter( + (pac) => pac.id !== p.id + ); + setPacientes(newPacientes); + alert(`Paciente ID: ${p.id} excluído`); + }} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + + Excluir + + + +
+ )) + )} +
+ {/* Paginação */} + {totalPages > 1 && ( +
+ {/* Botão Anterior */} + -
- {/* Tabela para Telas Médias e Grandes */} -
{/* Esconde em telas pequenas */} - - - - - - - - - - - - - - {loading ? ( - - - - ) : error ? ( - - - - ) : pacientes.length === 0 ? ( - - - - ) : ( - currentItems.map((p) => ( - - - - - - - - - - )) - )} - -
Nome - Telefone - - Cidade - - Estado - - Último atendimento - - Próximo atendimento - Ações
- - Carregando pacientes... -
{`Erro: ${error}`}
- Nenhum paciente encontrado -
{p.nome} - {p.telefone} - - {p.cidade} - - {p.estado} - - {p.ultimoAtendimento} - - {p.proximoAtendimento} - - - - - - - handleOpenModal(p)}> - - Ver detalhes - - - - - Laudos - - - alert(`Agenda para paciente ID: ${p.id}`)}> - - Ver agenda - - { - const newPacientes = pacientes.filter((pac) => pac.id !== p.id); - setPacientes(newPacientes); - alert(`Paciente ID: ${p.id} excluído`); - }} - className="text-red-600 focus:bg-red-50 focus:text-red-600" - > - - Excluir - - - -
-
+ {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} - {/* Layout em Cards/Lista para Telas Pequenas */} -
{/* Visível apenas em telas pequenas */} - {loading ? ( -
- - Carregando pacientes... -
- ) : error ? ( -
{`Erro: ${error}`}
- ) : pacientes.length === 0 ? ( -
- Nenhum paciente encontrado -
- ) : ( - currentItems.map((p) => ( -
-
{/* Adicionado padding à direita */} -
{/* Aumentado a fonte e break-words para evitar corte do nome */} - {p.nome || "—"} -
- {/* Removido o 'truncate' e adicionado 'break-words' no telefone */} -
- Telefone: **{p.telefone || "N/A"}** -
-
-
- - - - - - handleOpenModal(p)}> - - Ver detalhes - - - - - Laudos - - - alert(`Agenda para paciente ID: ${p.id}`)}> - - Ver agenda - - { - const newPacientes = pacientes.filter((pac) => pac.id !== p.id); - setPacientes(newPacientes); - alert(`Paciente ID: ${p.id} excluído`); - }} - className="text-red-600 focus:bg-red-50 focus:text-red-600" - > - - Excluir - - - -
-
- )) - )} -
- - - {/* Paginação */} - {totalPages > 1 && ( -
- - {/* Botão Anterior */} - - - {/* Números das Páginas */} - {visiblePageNumbers.map((number) => ( - - ))} - - {/* Botão Próximo */} - - -
- )} -
+ {/* Botão Próximo */} +
+ )} +
+
); -} \ No newline at end of file +} diff --git a/app/login/page.tsx b/app/login/page.tsx index 4be0708..4af96d9 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -138,7 +138,7 @@ export default function LoginPage() { Não tem uma conta de paciente?{" "} - + Crie uma agora @@ -232,18 +232,21 @@ export default function LoginPage() { {/* Botões */}
+ {/* Botão Cancelar – Azul contornado */} + + {/* Botão Resetar Senha – Azul sólido */} diff --git a/app/manager/dashboard/page.tsx b/app/manager/dashboard/page.tsx index 6558bd0..6562eef 100644 --- a/app/manager/dashboard/page.tsx +++ b/app/manager/dashboard/page.tsx @@ -1,6 +1,12 @@ "use client"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Calendar, Clock, Plus, User } from "lucide-react"; import Link from "next/link"; @@ -10,181 +16,212 @@ import { doctorsService } from "services/doctorsApi.mjs"; import Sidebar from "@/components/Sidebar"; export default function ManagerDashboard() { - // 🔹 Estados para usuários - const [firstUser, setFirstUser] = useState(null); - const [loadingUser, setLoadingUser] = useState(true); + // 🔹 Estados para usuários + const [firstUser, setFirstUser] = useState(null); + const [loadingUser, setLoadingUser] = useState(true); - // 🔹 Estados para médicos - const [doctors, setDoctors] = useState([]); - const [loadingDoctors, setLoadingDoctors] = useState(true); + // 🔹 Estados para médicos + const [doctors, setDoctors] = useState([]); + const [loadingDoctors, setLoadingDoctors] = useState(true); - // 🔹 Buscar primeiro usuário - useEffect(() => { - async function fetchFirstUser() { - try { - const data = await usersService.list_roles(); - if (Array.isArray(data) && data.length > 0) { - setFirstUser(data[0]); - } - } catch (error) { - console.error("Erro ao carregar usuário:", error); - } finally { - setLoadingUser(false); - } + // 🔹 Buscar primeiro usuário + useEffect(() => { + async function fetchFirstUser() { + try { + const data = await usersService.list_roles(); + if (Array.isArray(data) && data.length > 0) { + setFirstUser(data[0]); } + } catch (error) { + console.error("Erro ao carregar usuário:", error); + } finally { + setLoadingUser(false); + } + } - fetchFirstUser(); - }, []); + fetchFirstUser(); + }, []); - // 🔹 Buscar 3 primeiros médicos - useEffect(() => { - async function fetchDoctors() { - try { - const data = await doctorsService.list(); // ajuste se seu service tiver outro método - if (Array.isArray(data)) { - setDoctors(data.slice(0, 3)); // pega os 3 primeiros - } - } catch (error) { - console.error("Erro ao carregar médicos:", error); - } finally { - setLoadingDoctors(false); - } + // 🔹 Buscar 3 primeiros médicos + useEffect(() => { + async function fetchDoctors() { + try { + const data = await doctorsService.list(); // ajuste se seu service tiver outro método + if (Array.isArray(data)) { + setDoctors(data.slice(0, 3)); // pega os 3 primeiros } + } catch (error) { + console.error("Erro ao carregar médicos:", error); + } finally { + setLoadingDoctors(false); + } + } - fetchDoctors(); - }, []); + fetchDoctors(); + }, []); - return ( - -
- {/* Cabeçalho */} -
-

Dashboard

-

Bem-vindo ao seu portal de consultas médicas

+ return ( + +
+ {/* Cabeçalho */} +
+

Dashboard

+

+ Bem-vindo ao seu portal de consultas médicas +

+
+ + {/* Cards principais */} +
+ {/* Card 1 */} + + + + Relatórios gerenciais + + + + +
0
+

+ Relatórios disponíveis +

+
+
+ + {/* Card 2 — Gestão de usuários */} + + + + Gestão de usuários + + + + + {loadingUser ? ( +
+ Carregando usuário...
- - {/* Cards principais */} -
- {/* Card 1 */} - - - Relatórios gerenciais - - - -
0
-

Relatórios disponíveis

-
-
- - {/* Card 2 — Gestão de usuários */} - - - Gestão de usuários - - - - {loadingUser ? ( -
Carregando usuário...
- ) : firstUser ? ( - <> -
{firstUser.full_name || "Sem nome"}
-

- {firstUser.email || "Sem e-mail cadastrado"} -

- - ) : ( -
Nenhum usuário encontrado
- )} -
-
- - {/* Card 3 — Perfil */} - - - Perfil - - - -
100%
-

Dados completos

-
-
+ ) : firstUser ? ( + <> +
+ {firstUser.full_name || "Sem nome"} +
+

+ {firstUser.email || "Sem e-mail cadastrado"} +

+ + ) : ( +
+ Nenhum usuário encontrado
+ )} + + - {/* Cards secundários */} -
- {/* Card — Ações rápidas */} - - - Ações Rápidas - Acesse rapidamente as principais funcionalidades - - - - - - - - - - - - - - - - + {/* Card 3 — Perfil */} + + + Perfil + + + +
100%
+

Dados completos

+
+
+
- {/* Card — Gestão de Médicos */} - - - Gestão de Médicos - Médicos cadastrados recentemente - - - {loadingDoctors ? ( -

Carregando médicos...

- ) : doctors.length === 0 ? ( -

Nenhum médico cadastrado.

- ) : ( -
- {doctors.map((doc, index) => ( -
-
-

{doc.full_name || "Sem nome"}

-

- {doc.specialty || "Sem especialidade"} -

-
-
-

- {doc.active ? "Ativo" : "Inativo"} -

-
-
- ))} -
- )} -
-
+ {/* Cards secundários */} +
+ {/* Card — Ações rápidas */} + + + Ações Rápidas + + Acesse rapidamente as principais funcionalidades + + + + + + + + + + + + + + + + + + + {/* Card — Gestão de Médicos */} + + + Gestão de Médicos + + Médicos cadastrados recentemente + + + + {loadingDoctors ? ( +

Carregando médicos...

+ ) : doctors.length === 0 ? ( +

+ Nenhum médico cadastrado. +

+ ) : ( +
+ {doctors.map((doc, index) => ( +
+
+

+ {doc.full_name || "Sem nome"} +

+

+ {doc.specialty || "Sem especialidade"} +

+
+
+

+ {doc.active ? "Ativo" : "Inativo"} +

+
+
+ ))}
-
- - ); + )} + + +
+
+ + ); } 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/app/manager/home/page.tsx b/app/manager/home/page.tsx index afa89ed..55c0eff 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -1,480 +1,614 @@ "use client"; -import React, { useEffect, useState, useCallback, useMemo } from "react" -import Link from "next/link" +import React, { useEffect, useState, useCallback, useMemo } from "react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react" -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { doctorsService } from "services/doctorsApi.mjs"; import Sidebar from "@/components/Sidebar"; - interface Doctor { - id: number; - full_name: string; - specialty: string; - crm: string; - phone_mobile: string | null; - city: string | null; - state: string | null; - status?: string; + id: number; + full_name: string; + specialty: string; + crm: string; + phone_mobile: string | null; + city: string | null; + state: string | null; + status?: string; } interface DoctorDetails { - nome: string; - crm: string; - especialidade: string; - contato: { - celular?: string; - telefone1?: string; - }; - endereco: { - cidade?: string; - estado?: string; - }; - convenio?: string; - vip?: boolean; - status?: string; - ultimo_atendimento?: string; - proximo_atendimento?: string; - error?: string; + nome: string; + crm: string; + especialidade: string; + contato: { + celular?: string; + telefone1?: string; + }; + endereco: { + cidade?: string; + estado?: string; + }; + convenio?: string; + vip?: boolean; + status?: string; + ultimo_atendimento?: string; + proximo_atendimento?: string; + error?: string; } export default function DoctorsPage() { - const router = useRouter(); + const router = useRouter(); - const [doctors, setDoctors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [doctorDetails, setDoctorDetails] = useState(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); + const [doctors, setDoctors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [doctorDetails, setDoctorDetails] = useState( + null + ); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); - // --- Estados para Filtros --- - const [specialtyFilter, setSpecialtyFilter] = useState("all"); - const [statusFilter, setStatusFilter] = useState("all"); + // --- Estados para Filtros --- + const [specialtyFilter, setSpecialtyFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); - // --- Estados para Paginação --- - const [itemsPerPage, setItemsPerPage] = useState(10); - const [currentPage, setCurrentPage] = useState(1); + // --- Estados para Paginação --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); - const fetchDoctors = useCallback(async () => { - setLoading(true); - setError(null); - try { - const data: Doctor[] = await doctorsService.list(); - const dataWithStatus = data.map((doc, index) => ({ - ...doc, - status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo", - })); - setDoctors(dataWithStatus || []); - setCurrentPage(1); - } catch (e: any) { - console.error("Erro ao carregar lista de médicos:", e); - setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); - setDoctors([]); - } finally { - setLoading(false); - } - }, []); + const fetchDoctors = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data: Doctor[] = await doctorsService.list(); + const dataWithStatus = data.map((doc, index) => ({ + ...doc, + status: + index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo", + })); + setDoctors(dataWithStatus || []); + setCurrentPage(1); + } catch (e: any) { + console.error("Erro ao carregar lista de médicos:", e); + setError( + "Não foi possível carregar a lista de médicos. Verifique a conexão com a API." + ); + setDoctors([]); + } finally { + setLoading(false); + } + }, []); - useEffect(() => { - fetchDoctors(); - }, [fetchDoctors]); + useEffect(() => { + fetchDoctors(); + }, [fetchDoctors]); - const openDetailsDialog = async (doctor: Doctor) => { - setDetailsDialogOpen(true); - setDoctorDetails({ - nome: doctor.full_name, - crm: doctor.crm, - especialidade: doctor.specialty, - contato: { celular: doctor.phone_mobile ?? undefined }, - endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, - status: doctor.status || "Ativo", - convenio: "Particular", - vip: false, - ultimo_atendimento: "N/A", - proximo_atendimento: "N/A", - }); - }; - - const handleDelete = async () => { - if (doctorToDeleteId === null) return; - setLoading(true); - try { - await doctorsService.delete(doctorToDeleteId); - setDeleteDialogOpen(false); - setDoctorToDeleteId(null); - await fetchDoctors(); - } catch (e) { - console.error("Erro ao excluir:", e); - alert("Erro ao excluir médico."); - } finally { - setLoading(false); - } - }; - - const openDeleteDialog = (doctorId: number) => { - setDoctorToDeleteId(doctorId); - setDeleteDialogOpen(true); - }; - - const uniqueSpecialties = useMemo(() => { - const specialties = doctors.map((doctor) => doctor.specialty).filter(Boolean); - return [...new Set(specialties)]; - }, [doctors]); - - const filteredDoctors = doctors.filter((doctor) => { - const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; - const statusMatch = statusFilter === "all" || doctor.status === statusFilter; - return specialtyMatch && statusMatch; + const openDetailsDialog = async (doctor: Doctor) => { + setDetailsDialogOpen(true); + setDoctorDetails({ + nome: doctor.full_name, + crm: doctor.crm, + especialidade: doctor.specialty, + contato: { celular: doctor.phone_mobile ?? undefined }, + endereco: { + cidade: doctor.city ?? undefined, + estado: doctor.state ?? undefined, + }, + status: doctor.status || "Ativo", + convenio: "Particular", + vip: false, + ultimo_atendimento: "N/A", + proximo_atendimento: "N/A", }); + }; - const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + const handleDelete = async () => { + if (doctorToDeleteId === null) return; + setLoading(true); + try { + await doctorsService.delete(doctorToDeleteId); + setDeleteDialogOpen(false); + setDoctorToDeleteId(null); + await fetchDoctors(); + } catch (e) { + console.error("Erro ao excluir:", e); + alert("Erro ao excluir médico."); + } finally { + setLoading(false); + } + }; - const goToPrevPage = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; + const openDeleteDialog = (doctorId: number) => { + setDoctorToDeleteId(doctorId); + setDeleteDialogOpen(true); + }; - const goToNextPage = () => { - setCurrentPage((prev) => Math.min(totalPages, prev + 1)); - }; + const uniqueSpecialties = useMemo(() => { + const specialties = doctors + .map((doctor) => doctor.specialty) + .filter(Boolean); + return [...new Set(specialties)]; + }, [doctors]); - const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { - const pages: number[] = []; - const maxVisiblePages = 5; - const halfRange = Math.floor(maxVisiblePages / 2); - let startPage = Math.max(1, currentPage - halfRange); - let endPage = Math.min(totalPages, currentPage + halfRange); + const filteredDoctors = doctors.filter((doctor) => { + const specialtyMatch = + specialtyFilter === "all" || doctor.specialty === specialtyFilter; + const statusMatch = + statusFilter === "all" || doctor.status === statusFilter; + return specialtyMatch && statusMatch; + }); - if (endPage - startPage + 1 < maxVisiblePages) { - if (endPage === totalPages) { - startPage = Math.max(1, totalPages - maxVisiblePages + 1); - } - if (startPage === 1) { - endPage = Math.min(totalPages, maxVisiblePages); - } - } + const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - return pages; - }; + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; - const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(Number(value)); - setCurrentPage(1); - }; + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); - return ( - -
- {/* Cabeçalho */} -
-
-

Médicos Cadastrados

-

Gerencie todos os profissionais de saúde.

-
-
+ if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } - {/* Filtros e Itens por Página */} -
-
- Especialidade - -
-
- Status - -
-
- Itens por página - -
- -
+ for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; - {/* Tabela de Médicos (Visível em Telas Médias e Maiores) */} -
- {loading ? ( -
- - Carregando médicos... -
- ) : error ? ( -
{error}
- ) : filteredDoctors.length === 0 ? ( -
- {doctors.length === 0 - ? <>Nenhum médico cadastrado. Adicione um novo. - : "Nenhum médico encontrado com os filtros aplicados." - } -
- ) : ( -
- - - - - - - - - - - - - {currentItems.map((doctor) => ( - - - - - - - - - ))} - -
NomeCRMEspecialidadeStatusCidade/EstadoAções
{doctor.full_name}{doctor.crm}{doctor.specialty}{doctor.status || "N/A"} - {(doctor.city || doctor.state) - ? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}` - : "N/A"} - - - -
Ações
-
- - openDetailsDialog(doctor)}> - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(doctor.id)}> - - Excluir - - -
-
-
- )} -
+ const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); - {/* Cards de Médicos (Visível Apenas em Telas Pequenas) */} -
- {loading ? ( -
- - Carregando médicos... -
- ) : error ? ( -
{error}
- ) : filteredDoctors.length === 0 ? ( -
- {doctors.length === 0 - ? <>Nenhum médico cadastrado. Adicione um novo. - : "Nenhum médico encontrado com os filtros aplicados." - } -
- ) : ( -
- {currentItems.map((doctor) => ( -
-
-
{doctor.full_name}
-
{doctor.specialty}
-
- - -
Ações
-
- - openDetailsDialog(doctor)}> - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(doctor.id)}> - - Excluir - - -
-
- ))} -
- )} -
+ const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; - {/* Paginação */} - {totalPages > 1 && ( -
- + return ( + +
+ {/* Cabeçalho */} +
+
+

+ Médicos Cadastrados +

+

+ Gerencie todos os profissionais de saúde. +

+
+
- {visiblePageNumbers.map((number) => ( - - ))} + {/* Filtros e Itens por Página */} +
+
+ + Especialidade + + +
+
+ Status + +
+
+ + Itens por página + + +
+ +
- -
- )} - - {/* Dialogs de Exclusão e Detalhes */} - - - - Confirma a exclusão? - Esta ação é irreversível e excluirá permanentemente o registro deste médico. - - - Cancelar - - {loading ? : null} - Excluir - - - - - - - - - {doctorDetails?.nome} - - {doctorDetails && ( -
-

Informações Principais

-
-
- CRM: {doctorDetails.crm} -
-
- Especialidade: {doctorDetails.especialidade} -
-
- Celular: {doctorDetails.contato.celular || "N/A"} -
-
- Localização: {`${doctorDetails.endereco.cidade || "N/A"}/${doctorDetails.endereco.estado || "N/A"}`} -
-
- -

Atendimento e Convênio

-
-
- Convênio: {doctorDetails.convenio || "N/A"} -
-
- VIP: {doctorDetails.vip ? "Sim" : "Não"} -
-
- Status: {doctorDetails.status || "N/A"} -
-
- Último atendimento: {doctorDetails.ultimo_atendimento || "N/A"} -
-
- Próximo atendimento: {doctorDetails.proximo_atendimento || "N/A"} -
-
-
- )} - {doctorDetails === null && !loading &&
Detalhes não disponíveis.
} -
-
- - Fechar - -
-
+ {/* Tabela de Médicos (Visível em Telas Médias e Maiores) */} +
+ {loading ? ( +
+ + Carregando médicos...
- - ); -} \ No newline at end of file + ) : error ? ( +
{error}
+ ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 ? ( + <> + Nenhum médico cadastrado.{" "} + + Adicione um novo + + . + + ) : ( + "Nenhum médico encontrado com os filtros aplicados." + )} +
+ ) : ( +
+ + + + + + + + + + + + + {currentItems.map((doctor) => ( + + + + + + + + + ))} + +
+ Nome + + CRM + + Especialidade + + Status + + Cidade/Estado + + Ações +
+ {doctor.full_name} + + {doctor.crm} + + {doctor.specialty} + + {doctor.status || "N/A"} + + {doctor.city || doctor.state + ? `${doctor.city || ""}${ + doctor.city && doctor.state ? "/" : "" + }${doctor.state || ""}` + : "N/A"} + + + +
+ Ações +
+
+ + openDetailsDialog(doctor)} + > + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + openDeleteDialog(doctor.id)} + > + + Excluir + + +
+
+
+ )} +
+ + {/* Cards de Médicos (Visível Apenas em Telas Pequenas) */} +
+ {loading ? ( +
+ + Carregando médicos... +
+ ) : error ? ( +
{error}
+ ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 ? ( + <> + Nenhum médico cadastrado.{" "} + + Adicione um novo + + . + + ) : ( + "Nenhum médico encontrado com os filtros aplicados." + )} +
+ ) : ( +
+ {currentItems.map((doctor) => ( +
+
+
+ {doctor.full_name} +
+
+ {doctor.specialty} +
+
+ + +
+ Ações +
+
+ + openDetailsDialog(doctor)} + > + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + openDeleteDialog(doctor.id)} + > + + Excluir + + +
+
+ ))} +
+ )} +
+ + {/* Paginação */} + {totalPages > 1 && ( +
+ + + {visiblePageNumbers.map((number) => ( + + ))} + + +
+ )} + + {/* Dialogs de Exclusão e Detalhes */} + + + + Confirma a exclusão? + + Esta ação é irreversível e excluirá permanentemente o registro + deste médico. + + + + Cancelar + + {loading ? ( + + ) : null} + Excluir + + + + + + + + + + {doctorDetails?.nome} + + + {doctorDetails && ( +
+

+ Informações Principais +

+
+
+ CRM: {doctorDetails.crm} +
+
+ Especialidade:{" "} + {doctorDetails.especialidade} +
+
+ Celular:{" "} + {doctorDetails.contato.celular || "N/A"} +
+
+ Localização:{" "} + {`${doctorDetails.endereco.cidade || "N/A"}/${ + doctorDetails.endereco.estado || "N/A" + }`} +
+
+ +

+ Atendimento e Convênio +

+
+
+ Convênio:{" "} + {doctorDetails.convenio || "N/A"} +
+
+ VIP:{" "} + {doctorDetails.vip ? "Sim" : "Não"} +
+
+ Status: {doctorDetails.status || "N/A"} +
+
+ Último atendimento:{" "} + {doctorDetails.ultimo_atendimento || "N/A"} +
+
+ Próximo atendimento:{" "} + {doctorDetails.proximo_atendimento || "N/A"} +
+
+
+ )} + {doctorDetails === null && !loading && ( +
Detalhes não disponíveis.
+ )} +
+
+ + Fechar + +
+
+
+ + ); +} diff --git a/app/manager/pacientes/page.tsx b/app/manager/pacientes/page.tsx index 55b85cf..82d5d57 100644 --- a/app/manager/pacientes/page.tsx +++ b/app/manager/pacientes/page.tsx @@ -3,504 +3,625 @@ import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { patientsService } from "@/services/patientsApi.mjs"; import Sidebar from "@/components/Sidebar"; // Defina o tamanho da página. -const PAGE_SIZE = 5; +const PAGE_SIZE = 5; export default function PacientesPage() { - // --- ESTADOS DE DADOS E GERAL --- - const [searchTerm, setSearchTerm] = useState(""); - const [convenioFilter, setConvenioFilter] = useState("all"); - const [vipFilter, setVipFilter] = useState("all"); - - // Lista completa, carregada da API uma única vez - const [allPatients, setAllPatients] = useState([]); - // Lista após a aplicação dos filtros (base para a paginação) - const [filteredPatients, setFilteredPatients] = useState([]); + // --- ESTADOS DE DADOS E GERAL --- + const [searchTerm, setSearchTerm] = useState(""); + const [convenioFilter, setConvenioFilter] = useState("all"); + const [vipFilter, setVipFilter] = useState("all"); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // --- ESTADOS DE PAGINAÇÃO --- - const [page, setPage] = useState(1); - - // CÁLCULO DA PAGINAÇÃO - const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); - const startIndex = (page - 1) * PAGE_SIZE; - const endIndex = startIndex + PAGE_SIZE; - // Pacientes a serem exibidos na tabela (aplicando a paginação) - const currentPatients = filteredPatients.slice(startIndex, endIndex); + // Lista completa, carregada da API uma única vez + const [allPatients, setAllPatients] = useState([]); + // Lista após a aplicação dos filtros (base para a paginação) + const [filteredPatients, setFilteredPatients] = useState([]); - // --- ESTADOS DE DIALOGS --- - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [patientToDelete, setPatientToDelete] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [patientDetails, setPatientDetails] = useState(null); - - // --- FUNÇÕES DE LÓGICA --- + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // 1. Função para carregar TODOS os pacientes da API - const fetchAllPacientes = useCallback( - async () => { - setLoading(true); - setError(null); - try { - // Como o backend retorna um array, chamamos sem paginação - const res = await patientsService.list(); - - const mapped = res.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "—", - telefone: p.phone_mobile ?? p.phone1 ?? "—", - cidade: p.city ?? "—", - estado: p.state ?? "—", - // Formate as datas se necessário, aqui usamos como string - ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—", - proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—", - vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "Particular", // Define um valor padrão - status: p.status ?? undefined, - })); + // --- ESTADOS DE PAGINAÇÃO --- + const [page, setPage] = useState(1); - setAllPatients(mapped); - } catch (e: any) { - console.error(e); - setError(e?.message || "Erro ao buscar pacientes"); - } finally { - setLoading(false); - } - }, - [] - ); + // CÁLCULO DA PAGINAÇÃO + const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); + const startIndex = (page - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + // Pacientes a serem exibidos na tabela (aplicando a paginação) + const currentPatients = filteredPatients.slice(startIndex, endIndex); - // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) - useEffect(() => { - const filtered = allPatients.filter((patient) => { - // Filtro por termo de busca (Nome ou Telefone) - const matchesSearch = - patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || - patient.telefone?.includes(searchTerm); - - // Filtro por Convênio - const matchesConvenio = - convenioFilter === "all" || - patient.convenio === convenioFilter; - - // Filtro por VIP - const matchesVip = - vipFilter === "all" || - (vipFilter === "vip" && patient.vip) || - (vipFilter === "regular" && !patient.vip); + // --- ESTADOS DE DIALOGS --- + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [patientDetails, setPatientDetails] = useState(null); - return matchesSearch && matchesConvenio && matchesVip; - }); - - setFilteredPatients(filtered); - // Garante que a página atual seja válida após a filtragem - setPage(1); - }, [allPatients, searchTerm, convenioFilter, vipFilter]); + // --- FUNÇÕES DE LÓGICA --- - // 3. Efeito inicial para buscar os pacientes - useEffect(() => { - fetchAllPacientes(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // 1. Função para carregar TODOS os pacientes da API + const fetchAllPacientes = useCallback(async () => { + setLoading(true); + setError(null); + try { + // Como o backend retorna um array, chamamos sem paginação + const res = await patientsService.list(); + const mapped = res.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? p.phone1 ?? "—", + cidade: p.city ?? "—", + estado: p.state ?? "—", + // Formate as datas se necessário, aqui usamos como string + ultimoAtendimento: p.last_visit_at?.split("T")[0] ?? "—", + proximoAtendimento: p.next_appointment_at?.split("T")[0] ?? "—", + vip: Boolean(p.vip ?? false), + convenio: p.convenio ?? "Particular", // Define um valor padrão + status: p.status ?? undefined, + })); - // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- - - const openDetailsDialog = async (patientId: string) => { - setDetailsDialogOpen(true); - setPatientDetails(null); - try { - const res = await patientsService.getById(patientId); - setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item - } catch (e: any) { - setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); - } - }; + setAllPatients(mapped); + } catch (e: any) { + console.error(e); + setError(e?.message || "Erro ao buscar pacientes"); + } finally { + setLoading(false); + } + }, []); - const handleDeletePatient = async (patientId: string) => { - try { - await patientsService.delete(patientId); - // Atualiza a lista completa para refletir a exclusão - setAllPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId))); - } catch (e: any) { - alert(`Erro ao deletar paciente: ${e?.message || 'Erro desconhecido'}`); - } - setDeleteDialogOpen(false); - setPatientToDelete(null); - }; + // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) + useEffect(() => { + const filtered = allPatients.filter((patient) => { + // Filtro por termo de busca (Nome ou Telefone) + const matchesSearch = + patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || + patient.telefone?.includes(searchTerm); - const openDeleteDialog = (patientId: string) => { - setPatientToDelete(patientId); - setDeleteDialogOpen(true); - }; + // Filtro por Convênio + const matchesConvenio = + convenioFilter === "all" || patient.convenio === convenioFilter; - return ( - -
- {/* Header (Responsividade OK) */} -
-
-

Pacientes

-

Gerencie as informações de seus pacientes

-
-
+ // Filtro por VIP + const matchesVip = + vipFilter === "all" || + (vipFilter === "vip" && patient.vip) || + (vipFilter === "regular" && !patient.vip); - {/* Bloco de Filtros (Responsividade APLICADA) */} - {/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */} -
- - - {/* Busca - Ocupa 100% no mobile, depois cresce */} - setSearchTerm(e.target.value)} - // w-full no mobile, depois flex-grow para ocupar o espaço disponível - className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm" - /> + return matchesSearch && matchesConvenio && matchesVip; + }); - {/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */} -
- Convênio - -
+ setFilteredPatients(filtered); + // Garante que a página atual seja válida após a filtragem + setPage(1); + }, [allPatients, searchTerm, convenioFilter, vipFilter]); - {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} -
- VIP - -
- - {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} - -
+ // 3. Efeito inicial para buscar os pacientes + useEffect(() => { + fetchAllPacientes(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - {/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} - {/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */} -
-
{/* Permite rolagem horizontal se a tabela for muito larga */} - {error ? ( -
{`Erro ao carregar pacientes: ${error}`}
- ) : loading ? ( -
- Carregando pacientes... + // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- + + const openDetailsDialog = async (patientId: string) => { + setDetailsDialogOpen(true); + setPatientDetails(null); + try { + const res = await patientsService.getById(patientId); + setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item + } catch (e: any) { + setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); + } + }; + + const handleDeletePatient = async (patientId: string) => { + try { + await patientsService.delete(patientId); + // Atualiza a lista completa para refletir a exclusão + setAllPatients((prev) => + prev.filter((p) => String(p.id) !== String(patientId)) + ); + } catch (e: any) { + alert(`Erro ao deletar paciente: ${e?.message || "Erro desconhecido"}`); + } + setDeleteDialogOpen(false); + setPatientToDelete(null); + }; + + const openDeleteDialog = (patientId: string) => { + setPatientToDelete(patientId); + setDeleteDialogOpen(true); + }; + + return ( + +
+ {/* Header (Responsividade OK) */} +
+
+

+ Pacientes +

+

+ Gerencie as informações de seus pacientes +

+
+
+ + {/* Bloco de Filtros (Responsividade APLICADA) */} + {/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */} +
+ + + {/* Busca - Ocupa 100% no mobile, depois cresce */} + setSearchTerm(e.target.value)} + // w-full no mobile, depois flex-grow para ocupar o espaço disponível + className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm" + /> + + {/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */} +
+ + Convênio + + +
+ + {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} +
+ + VIP + + +
+ + {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} + +
+ + {/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} + {/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */} +
+
+ {" "} + {/* Permite rolagem horizontal se a tabela for muito larga */} + {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : loading ? ( +
+ {" "} + Carregando pacientes... +
+ ) : ( + + {" "} + {/* min-w para evitar que a tabela se contraia demais */} + + + + {/* Ajustes de visibilidade de colunas para diferentes breakpoints */} + + + + + + + + + + {currentPatients.length === 0 ? ( + + + + ) : ( + currentPatients.map((patient) => ( + + + + )) + )} + +
+ Nome + + Telefone + + Cidade / Estado + + Convênio + + Último atendimento + + Próximo atendimento + + Ações +
+ {allPatients.length === 0 + ? "Nenhum paciente cadastrado" + : "Nenhum paciente encontrado com os filtros aplicados"} +
+
+
+ + {patient.nome?.charAt(0) || "?"} +
- ) : ( - {/* min-w para evitar que a tabela se contraia demais */} - - - - {/* Ajustes de visibilidade de colunas para diferentes breakpoints */} - - - - - - - - - - {currentPatients.length === 0 ? ( - - - - ) : ( - currentPatients.map((patient) => ( - - - - - - - - - + + + + + - - - Marcar consulta - - openDeleteDialog(String(patient.id))}> - - Excluir - - - - - - )) - )} - -
NomeTelefoneCidade / EstadoConvênioÚltimo atendimentoPróximo atendimentoAções
- {allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
-
-
- {patient.nome?.charAt(0) || "?"} -
- - {patient.nome} - {patient.vip && ( - VIP - )} - -
-
{patient.telefone}{`${patient.cidade} / ${patient.estado}`}{patient.convenio}{patient.ultimoAtendimento}{patient.proximoAtendimento} - - -
Ações
-
- - openDetailsDialog(String(patient.id))}> - - Ver detalhes - - - - - Editar - - + + {patient.nome} + {patient.vip && ( + + VIP + + )} + + +
+ {patient.telefone} + {`${patient.cidade} / ${patient.estado}`} + {patient.convenio} + + {patient.ultimoAtendimento} + + {patient.proximoAtendimento} +
- )} -
- +
+ + +
+ Ações +
+
+ + + openDetailsDialog(String(patient.id)) + } + > + + Ver detalhes + - {/* --- SEÇÃO DE CARDS (VISÍVEL APENAS EM TELAS MENORES QUE MD) --- */} - {/* Garantir que os cards apareçam em telas menores e se escondam em MD+ */} -
- {error ? ( -
{`Erro ao carregar pacientes: ${error}`}
- ) : loading ? ( -
- Carregando pacientes... -
- ) : filteredPatients.length === 0 ? ( -
- {allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
- ) : ( -
- {currentPatients.map((patient) => ( -
-
-
- {patient.nome} - {patient.vip && ( - VIP - )} -
-
Telefone: {patient.telefone}
-
Convênio: {patient.convenio}
-
- - -
-
- - openDetailsDialog(String(patient.id))}> - - Ver detalhes - + + + + Editar + + - - - - Editar - - - - - - Marcar consulta - - openDeleteDialog(String(patient.id))}> - - Excluir - - -
-
- ))} -
- )} -
- - {/* Paginação */} - {totalPages > 1 && !loading && ( -
-
{/* Adicionado flex-wrap e justify-center para botões da paginação */} - - - {Array.from({ length: totalPages }, (_, index) => index + 1) - .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) - .map((pageNumber) => ( - - ))} - - -
-
- )} - - {/* AlertDialogs (Permanecem os mesmos) */} - - - - Confirmar exclusão - Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. - - - Cancelar - patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + + + Marcar consulta + + + openDeleteDialog(String(patient.id)) + } + > + Excluir - - - - + +
+
+
+ )} +
+
- - - - Detalhes do Paciente - - {patientDetails === null ? ( -
- - Carregando... -
- ) : patientDetails?.error ? ( -
{patientDetails.error}
- ) : ( -
-
-
-

Nome Completo

-

{patientDetails.full_name}

-
-
-

Email

-

{patientDetails.email}

-
-
-

Telefone

-

{patientDetails.phone_mobile}

-
-
-

Data de Nascimento

-

{patientDetails.birth_date}

-
-
-

CPF

-

{patientDetails.cpf}

-
-
-

Tipo Sanguíneo

-

{patientDetails.blood_type}

-
-
-

Peso (kg)

-

{patientDetails.weight_kg}

-
-
-

Altura (m)

-

{patientDetails.height_m}

-
-
-
-

Endereço

-
-
-

Rua

-

{`${patientDetails.street}, ${patientDetails.number}`}

-
-
-

Complemento

-

{patientDetails.complement}

-
-
-

Bairro

-

{patientDetails.neighborhood}

-
-
-

Cidade

-

{patientDetails.cidade}

-
-
-

Estado

-

{patientDetails.estado}

-
-
-

CEP

-

{patientDetails.cep}

-
-
-
-
- )} -
-
- - Fechar - -
-
+ {/* --- SEÇÃO DE CARDS (VISÍVEL APENAS EM TELAS MENORES QUE MD) --- */} + {/* Garantir que os cards apareçam em telas menores e se escondam em MD+ */} +
+ {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : loading ? ( +
+ {" "} + Carregando pacientes...
- - ); -} \ No newline at end of file + ) : filteredPatients.length === 0 ? ( +
+ {allPatients.length === 0 + ? "Nenhum paciente cadastrado" + : "Nenhum paciente encontrado com os filtros aplicados"} +
+ ) : ( +
+ {currentPatients.map((patient) => ( +
+
+
+ {patient.nome} + {patient.vip && ( + + VIP + + )} +
+
+ Telefone: {patient.telefone} +
+
+ Convênio: {patient.convenio} +
+
+ + +
+ +
+
+ + openDetailsDialog(String(patient.id))} + > + + Ver detalhes + + + + + + Editar + + + + + + Marcar consulta + + openDeleteDialog(String(patient.id))} + > + + Excluir + + +
+
+ ))} +
+ )} +
+ + {/* Paginação */} + {totalPages > 1 && !loading && ( +
+
+ {" "} + {/* Adicionado flex-wrap e justify-center para botões da paginação */} + + {Array.from({ length: totalPages }, (_, index) => index + 1) + .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) + .map((pageNumber) => ( + + ))} + +
+
+ )} + + {/* AlertDialogs (Permanecem os mesmos) */} + + + + Confirmar exclusão + + Tem certeza que deseja excluir este paciente? Esta ação não pode + ser desfeita. + + + + Cancelar + + patientToDelete && handleDeletePatient(patientToDelete) + } + className="bg-red-600 hover:bg-red-700" + > + Excluir + + + + + + + + + Detalhes do Paciente + + {patientDetails === null ? ( +
+ + Carregando... +
+ ) : patientDetails?.error ? ( +
{patientDetails.error}
+ ) : ( +
+
+
+

Nome Completo

+

{patientDetails.full_name}

+
+
+

Email

+

{patientDetails.email}

+
+
+

Telefone

+

{patientDetails.phone_mobile}

+
+
+

Data de Nascimento

+

{patientDetails.birth_date}

+
+
+

CPF

+

{patientDetails.cpf}

+
+
+

Tipo Sanguíneo

+

{patientDetails.blood_type}

+
+
+

Peso (kg)

+

{patientDetails.weight_kg}

+
+
+

Altura (m)

+

{patientDetails.height_m}

+
+
+
+

Endereço

+
+
+

Rua

+

{`${patientDetails.street}, ${patientDetails.number}`}

+
+
+

Complemento

+

{patientDetails.complement}

+
+
+

Bairro

+

{patientDetails.neighborhood}

+
+
+

Cidade

+

{patientDetails.cidade}

+
+
+

Estado

+

{patientDetails.estado}

+
+
+

CEP

+

{patientDetails.cep}

+
+
+
+
+ )} +
+
+ + Fechar + +
+
+
+
+ ); +} diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index 9cc8bbc..53b557f 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -3,416 +3,451 @@ import React, { useEffect, useState, useCallback } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Plus, Eye, Filter, Loader2 } from "lucide-react"; -import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { api, login } from "services/api.mjs"; import { usersService } from "services/usersApi.mjs"; import Sidebar from "@/components/Sidebar"; interface FlatUser { - id: string; - user_id: string; - full_name?: string; - email: string; - phone?: string | null; - role: string; + id: string; + user_id: string; + full_name?: string; + email: string; + phone?: string | null; + role: string; } interface UserInfoResponse { - user: any; - profile: any; - roles: string[]; - permissions: Record; + user: any; + profile: any; + roles: string[]; + permissions: Record; } export default function UsersPage() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [userDetails, setUserDetails] = useState( - null - ); - const [selectedRole, setSelectedRole] = useState("all"); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [userDetails, setUserDetails] = useState(null); + const [selectedRole, setSelectedRole] = useState("all"); - // --- Lógica de Paginação INÍCIO --- - const [itemsPerPage, setItemsPerPage] = useState(10); - const [currentPage, setCurrentPage] = useState(1); + // --- Lógica de Paginação INÍCIO --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(Number(value)); - setCurrentPage(1); - }; - // --- Lógica de Paginação FIM --- + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + // --- Lógica de Paginação FIM --- - const fetchUsers = useCallback(async () => { - setLoading(true); - setError(null); - try { - const rolesData: any[] = await usersService.list_roles(); - const rolesArray = Array.isArray(rolesData) ? rolesData : []; + const fetchUsers = useCallback(async () => { + setLoading(true); + setError(null); + try { + const rolesData: any[] = await usersService.list_roles(); + const rolesArray = Array.isArray(rolesData) ? rolesData : []; - const profilesData: any[] = await api.get( - `/rest/v1/profiles?select=id,full_name,email,phone` - ); + const profilesData: any[] = await api.get( + `/rest/v1/profiles?select=id,full_name,email,phone` + ); - const profilesById = new Map(); - if (Array.isArray(profilesData)) { - for (const p of profilesData) { - if (p?.id) profilesById.set(p.id, p); - } - } - - const mapped: FlatUser[] = rolesArray.map((roleItem) => { - const uid = roleItem.user_id; - const profile = profilesById.get(uid); - return { - id: uid, - user_id: uid, - full_name: profile?.full_name ?? "—", - email: profile?.email ?? "—", - phone: profile?.phone ?? "—", - role: roleItem.role ?? "—", - }; - }); - - setUsers(mapped); - setCurrentPage(1); - } catch (err: any) { - console.error("Erro ao buscar usuários:", err); - setError("Não foi possível carregar os usuários. Veja console."); - setUsers([]); - } finally { - setLoading(false); + const profilesById = new Map(); + if (Array.isArray(profilesData)) { + for (const p of profilesData) { + if (p?.id) profilesById.set(p.id, p); } - }, []); + } - useEffect(() => { - const init = async () => { - try { - await login(); - } catch (e) { - console.warn("login falhou no init:", e); - } - await fetchUsers(); + const mapped: FlatUser[] = rolesArray.map((roleItem) => { + const uid = roleItem.user_id; + const profile = profilesById.get(uid); + return { + id: uid, + user_id: uid, + full_name: profile?.full_name ?? "—", + email: profile?.email ?? "—", + phone: profile?.phone ?? "—", + role: roleItem.role ?? "—", }; - init(); - }, [fetchUsers]); + }); - const openDetailsDialog = async (flatUser: FlatUser) => { - setDetailsDialogOpen(true); - setUserDetails(null); + setUsers(mapped); + setCurrentPage(1); + } catch (err: any) { + console.error("Erro ao buscar usuários:", err); + setError("Não foi possível carregar os usuários. Veja console."); + setUsers([]); + } finally { + setLoading(false); + } + }, []); - try { - const data = await usersService.full_data(flatUser.user_id); - setUserDetails(data); - } catch (err: any) { - console.error("Erro ao carregar detalhes:", err); - setUserDetails({ - user: { id: flatUser.user_id, email: flatUser.email }, - profile: { full_name: flatUser.full_name, phone: flatUser.phone }, - roles: [flatUser.role], - permissions: {}, - }); - } + useEffect(() => { + const init = async () => { + try { + await login(); + } catch (e) { + console.warn("login falhou no init:", e); + } + await fetchUsers(); }; + init(); + }, [fetchUsers]); - const filteredUsers = - selectedRole && selectedRole !== "all" - ? users.filter((u) => u.role === selectedRole) - : users; + const openDetailsDialog = async (flatUser: FlatUser) => { + setDetailsDialogOpen(true); + setUserDetails(null); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = filteredUsers.slice(indexOfFirstItem, indexOfLastItem); + try { + const data = await usersService.full_data(flatUser.user_id); + setUserDetails(data); + } catch (err: any) { + console.error("Erro ao carregar detalhes:", err); + setUserDetails({ + user: { id: flatUser.user_id, email: flatUser.email }, + profile: { full_name: flatUser.full_name, phone: flatUser.phone }, + roles: [flatUser.role], + permissions: {}, + }); + } + }; - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + const filteredUsers = + selectedRole && selectedRole !== "all" + ? users.filter((u) => u.role === selectedRole) + : users; - const totalPages = Math.ceil(filteredUsers.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredUsers.slice(indexOfFirstItem, indexOfLastItem); - const goToPrevPage = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); - const goToNextPage = () => { - setCurrentPage((prev) => Math.min(totalPages, prev + 1)); - }; + const totalPages = Math.ceil(filteredUsers.length / itemsPerPage); - const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { - const pages: number[] = []; - const maxVisiblePages = 5; - const halfRange = Math.floor(maxVisiblePages / 2); - let startPage = Math.max(1, currentPage - halfRange); - let endPage = Math.min(totalPages, currentPage + halfRange); + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; - if (endPage - startPage + 1 < maxVisiblePages) { - if (endPage === totalPages) { - startPage = Math.max(1, totalPages - maxVisiblePages + 1); - } - if (startPage === 1) { - endPage = Math.min(totalPages, maxVisiblePages); - } - } + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; - for (let i = startPage; i <= endPage; i++) { - pages.push(i); - } - return pages; - }; + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); - const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } - return ( - -
+ for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; - {/* Header */} -
-
-

Usuários

-

Gerencie usuários.

-
- - - -
+ const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); - {/* Filtro e Itens por Página */} -
+ return ( + +
+ {/* Header */} +
+
+

Usuários

+

Gerencie usuários.

+
+ + + +
- {/* Select de Filtro por Papel - Ajustado para resetar a página */} -
- - Filtrar por papel - - { + setSelectedRole(value); + setCurrentPage(1); + }} + value={selectedRole} + > + + {" "} + {/* w-full para mobile, w-[180px] para sm+ */} + + + + Todos + Admin + Gestor + Médico + Secretária + Usuário + + +
- {/* w-full para mobile, w-[180px] para sm+ */} - - - - Todos - Admin - Gestor - Médico - Secretária - Usuário - - -
+ {/* Select de Itens por Página */} +
+ + Itens por página + + +
+ +
+ {/* Fim do Filtro e Itens por Página */} - {/* Select de Itens por Página */} -
- - Itens por página - - -
- -
- {/* Fim do Filtro e Itens por Página */} - - {/* Tabela/Lista */} -
- {loading ? ( -
- - Carregando usuários... -
- ) : error ? ( -
{error}
- ) : filteredUsers.length === 0 ? ( -
- Nenhum usuário encontrado com os filtros aplicados. -
- ) : ( - <> - {/* Tabela para Telas Médias e Grandes */} - - - - - - - - - - - - {currentItems.map((u) => ( - - - - - - - - ))} - -
NomeE-mailTelefoneCargoAções
- {u.full_name} - - {u.email} - - {u.phone} - - {u.role} - - -
- - {/* Layout em Cards/Lista para Telas Pequenas */} -
- {currentItems.map((u) => ( -
-
-
- {u.full_name || "—"} -
-
- {u.role || "—"} -
-
-
- -
-
- ))} -
- - {/* Paginação */} - {totalPages > 1 && ( -
- - {/* Botão Anterior */} - - - {/* Números das Páginas */} - {visiblePageNumbers.map((number) => ( - - ))} - - {/* Botão Próximo */} - - -
- )} - - )} -
- - {/* Modal de Detalhes */} - - - - - {userDetails?.profile?.full_name || "Detalhes do Usuário"} - - - {!userDetails ? ( -
- - Buscando dados completos... -
- ) : ( -
-
- ID: {userDetails.user.id} -
-
- E-mail: {userDetails.user.email} -
-
- Nome completo:{" "} - {userDetails.profile.full_name} -
-
- Telefone: {userDetails.profile.phone} -
-
- Roles:{" "} - {userDetails.roles?.join(", ")} -
-
- Permissões: -
    - {Object.entries( - userDetails.permissions || {} - ).map(([k, v]) => ( -
  • - {k}: {v ? "Sim" : "Não"} -
  • - ))} -
-
-
- )} -
-
- - Fechar - -
-
+ {/* Tabela/Lista */} +
+ {loading ? ( +
+ + Carregando usuários...
- - ); -} \ No newline at end of file + ) : error ? ( +
{error}
+ ) : filteredUsers.length === 0 ? ( +
+ Nenhum usuário encontrado com os filtros aplicados. +
+ ) : ( + <> + {/* Tabela para Telas Médias e Grandes */} + + + + + + + + + + + + {currentItems.map((u) => ( + + + + + + + + ))} + +
+ Nome + + E-mail + + Telefone + + Cargo + + Ações +
+ {u.full_name} + + {u.email} + + {u.phone} + + {u.role} + + +
+ + {/* Layout em Cards/Lista para Telas Pequenas */} +
+ {currentItems.map((u) => ( +
+
+
+ {u.full_name || "—"} +
+
+ {u.role || "—"} +
+
+
+ +
+
+ ))} +
+ + {/* Paginação */} + {totalPages > 1 && ( +
+ {/* Botão Anterior */} + + + {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} + + {/* Botão Próximo */} + +
+ )} + + )} +
+ + {/* Modal de Detalhes */} + + + + + {userDetails?.profile?.full_name || "Detalhes do Usuário"} + + + {!userDetails ? ( +
+ + Buscando dados completos... +
+ ) : ( +
+
+ ID: {userDetails.user.id} +
+
+ E-mail: {userDetails.user.email} +
+
+ Nome completo:{" "} + {userDetails.profile.full_name} +
+
+ Telefone: {userDetails.profile.phone} +
+
+ Roles: {userDetails.roles?.join(", ")} +
+
+ Permissões: +
    + {Object.entries(userDetails.permissions || {}).map( + ([k, v]) => ( +
  • + {k}:{" "} + + {v ? "Sim" : "Não"} + +
  • + ) + )} +
+
+
+ )} +
+
+ + Fechar + +
+
+
+ + ); +} diff --git a/app/page.tsx b/app/page.tsx index 81fb7f2..6a6ce1f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,50 +3,49 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { useState } from "react"; +import { Stethoscope, Baby, Microscope } from "lucide-react"; export default function InicialPage() { const [isMenuOpen, setIsMenuOpen] = useState(false); return ( -
- {/* Barra superior de informações */} -
+
+ {/* Barra superior */} +
Horário: 08h00 - 21h00 - Email: contato@mediconnect.com + + Email: contato@mediconnect.com +
- - {/* Header principal - Com Logo REAL */} -
- {/* Agrupamento do Logo e Nome do Site */} - - {/* 1. IMAGEM/LOGO REAL: Referenciando o arquivo placeholder-logo.png na pasta public */} + {/* Header */} +
+ Logo MediConnect - - {/* 2. NOME DO SITE */} -

MedConnect

+

+ MedConnect +

- {/* Botão do menu hambúrguer para telas menores */} + {/* Menu Mobile */}
- {/* O botão de login agora estará sempre aqui, fora do menu */}
- {/* Navegação principal */} + {/* Navegação */} - {/* Botão de Login para telas maiores (md e acima) */} + {/* Login Desktop */}
- - {/* Seção principal de destaque */} -
+ {/* Hero Section */} +
-

+

Bem-vindo à Saúde Digital

-

+

Soluções Médicas
& Cuidados com a Saúde

-

+

Excelência em saúde há mais de 25 anos. Atendimento médico com qualidade, segurança e carinho.

-
- - +
+ +
Médico
- - {/* Seção de serviços */} -
-

+ {/* Serviços */} +
+

Cuidados completos para a sua saúde

-

+

Serviços médicos que oferecemos

-
-
-

- Clínica Geral -

-

- Seu primeiro passo para o cuidado. Atendimento focado na prevenção - e no diagnóstico inicial. -

- -
-
-

Pediatria

-

- Cuidado gentil e especializado para garantir a saúde e o - desenvolvimento de crianças e adolescentes. -

- -
-
-

Exames

-

- Resultados rápidos e precisos em exames laboratoriais e de imagem - essenciais para seu diagnóstico. -

- -
+
+ {/* Card */} + {[ + { + title: "Clínica Geral", + desc: "Seu primeiro passo para o cuidado. Atendimento focado na prevenção e no diagnóstico inicial.", + Icon: Stethoscope, + }, + { + title: "Pediatria", + desc: "Cuidado gentil e especializado para garantir a saúde e o desenvolvimento de crianças e adolescentes.", + Icon: Baby, + }, + { + title: "Exames", + desc: "Resultados rápidos e precisos em exames laboratoriais e de imagem essenciais para seu diagnóstico.", + Icon: Microscope, + }, + ].map(({ title, desc, Icon }, index) => ( +
+
+ +

{title}

+
+

+ {desc} +

+ +
+ ))}
- {/* Footer */} -

); } diff --git a/app/patient/dashboard/page.tsx b/app/patient/dashboard/page.tsx index c0f9266..6573d1e 100644 --- a/app/patient/dashboard/page.tsx +++ b/app/patient/dashboard/page.tsx @@ -1,8 +1,14 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Calendar, Clock, User, Plus } from "lucide-react" -import Link from "next/link" -import Sidebar from "@/components/Sidebar" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, User, Plus } from "lucide-react"; +import Link from "next/link"; +import Sidebar from "@/components/Sidebar"; export default function PatientDashboard() { return ( @@ -10,13 +16,17 @@ export default function PatientDashboard() {

Dashboard

-

Bem-vindo ao seu portal de consultas médicas

+

+ Bem-vindo ao seu portal de consultas médicas +

- Próxima Consulta + + Próxima Consulta + @@ -27,12 +37,16 @@ export default function PatientDashboard() { - Consultas Este Mês + + Consultas Este Mês +
3
-

2 realizadas, 1 agendada

+

+ 2 realizadas, 1 agendada +

@@ -52,23 +66,31 @@ export default function PatientDashboard() { Ações Rápidas - Acesse rapidamente as principais funcionalidades + + Acesse rapidamente as principais funcionalidades + - - - @@ -109,5 +131,5 @@ export default function PatientDashboard() {
- ) + ); } diff --git a/app/patient/profile/page.tsx b/app/patient/profile/page.tsx index 6b42c07..46911b9 100644 --- a/app/patient/profile/page.tsx +++ b/app/patient/profile/page.tsx @@ -18,243 +18,359 @@ import { toast } from "@/hooks/use-toast"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; interface PatientProfileData { - name: string; - email: string; - phone: string; - cpf: string; - birthDate: string; - cep: string; - street: string; - number: string; - city: string; - avatarFullUrl?: string; + name: string; + email: string; + phone: string; + cpf: string; + birthDate: string; + cep: string; + street: string; + number: string; + city: string; + avatarFullUrl?: string; } export default function PatientProfile() { - const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: ["paciente", "admin", "medico", "gestor", "secretaria"] }); - const [patientData, setPatientData] = useState(null); - const [isEditing, setIsEditing] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const fileInputRef = useRef(null); + const { user, isLoading: isAuthLoading } = useAuthLayout({ + requiredRole: ["paciente", "admin", "medico", "gestor", "secretaria"], + }); + const [patientData, setPatientData] = useState( + null + ); + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const fileInputRef = useRef(null); - useEffect(() => { - if (user?.id) { - const fetchPatientDetails = async () => { - try { - const patientDetails = await patientsService.getById(user.id); - setPatientData({ - name: patientDetails.full_name || user.name, - email: user.email, - phone: patientDetails.phone_mobile || "", - cpf: patientDetails.cpf || "", - birthDate: patientDetails.birth_date || "", - cep: patientDetails.cep || "", - street: patientDetails.street || "", - number: patientDetails.number || "", - city: patientDetails.city || "", - avatarFullUrl: user.avatarFullUrl, - }); - } catch (error) { - console.error("Erro ao buscar detalhes do paciente:", error); - toast({ title: "Erro", description: "Não foi possível carregar seus dados completos.", variant: "destructive" }); - } - }; - fetchPatientDetails(); - } - }, [user]); - - const handleInputChange = (field: keyof PatientProfileData, value: string) => { - setPatientData((prev) => (prev ? { ...prev, [field]: value } : null)); - }; - - const handleSave = async () => { - if (!patientData || !user) return; - setIsSaving(true); + useEffect(() => { + if (user?.id) { + const fetchPatientDetails = async () => { try { - const patientPayload = { - full_name: patientData.name, - cpf: patientData.cpf, - birth_date: patientData.birthDate, - phone_mobile: patientData.phone, - cep: patientData.cep, - street: patientData.street, - number: patientData.number, - city: patientData.city, - }; - await patientsService.update(user.id, patientPayload); - toast({ title: "Sucesso!", description: "Seus dados foram atualizados." }); - setIsEditing(false); + const patientDetails = await patientsService.getById(user.id); + setPatientData({ + name: patientDetails.full_name || user.name, + email: user.email, + phone: patientDetails.phone_mobile || "", + cpf: patientDetails.cpf || "", + birthDate: patientDetails.birth_date || "", + cep: patientDetails.cep || "", + street: patientDetails.street || "", + number: patientDetails.number || "", + city: patientDetails.city || "", + avatarFullUrl: user.avatarFullUrl, + }); } catch (error) { - console.error("Erro ao salvar dados:", error); - toast({ title: "Erro", description: "Não foi possível salvar suas alterações.", variant: "destructive" }); - } finally { - setIsSaving(false); + console.error("Erro ao buscar detalhes do paciente:", error); + toast({ + title: "Erro", + description: "Não foi possível carregar seus dados completos.", + variant: "destructive", + }); } - }; - - const handleAvatarClick = () => { - fileInputRef.current?.click(); - }; - - const handleAvatarUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file || !user) return; - - const fileExt = file.name.split(".").pop(); - - // *** A CORREÇÃO ESTÁ AQUI *** - // O caminho salvo no banco de dados não deve conter o nome do bucket. - const filePath = `${user.id}/avatar.${fileExt}`; - - try { - await api.storage.upload("avatars", filePath, file); - await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { avatar_url: filePath }); - - const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`; - setPatientData((prev) => (prev ? { ...prev, avatarFullUrl: newFullUrl } : null)); - - toast({ title: "Sucesso!", description: "Sua foto de perfil foi atualizada." }); - } catch (error) { - console.error("Erro no upload do avatar:", error); - toast({ title: "Erro de Upload", description: "Não foi possível enviar sua foto.", variant: "destructive" }); - } - }; - - if (isAuthLoading || !patientData) { - return ( - -
Carregando seus dados...
-
- ); + }; + fetchPatientDetails(); } + }, [user]); + const handleInputChange = ( + field: keyof PatientProfileData, + value: string + ) => { + setPatientData((prev) => (prev ? { ...prev, [field]: value } : null)); + }; + + const handleSave = async () => { + if (!patientData || !user) return; + setIsSaving(true); + try { + const patientPayload = { + full_name: patientData.name, + cpf: patientData.cpf, + birth_date: patientData.birthDate, + phone_mobile: patientData.phone, + cep: patientData.cep, + street: patientData.street, + number: patientData.number, + city: patientData.city, + }; + await patientsService.update(user.id, patientPayload); + toast({ + title: "Sucesso!", + description: "Seus dados foram atualizados.", + }); + setIsEditing(false); + } catch (error) { + console.error("Erro ao salvar dados:", error); + toast({ + title: "Erro", + description: "Não foi possível salvar suas alterações.", + variant: "destructive", + }); + } finally { + setIsSaving(false); + } + }; + + const handleAvatarClick = () => { + fileInputRef.current?.click(); + }; + + const handleAvatarUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (!file || !user) return; + + const fileExt = file.name.split(".").pop(); + + // *** A CORREÇÃO ESTÁ AQUI *** + // O caminho salvo no banco de dados não deve conter o nome do bucket. + const filePath = `${user.id}/avatar.${fileExt}`; + + try { + await api.storage.upload("avatars", filePath, file); + await api.patch(`/rest/v1/profiles?id=eq.${user.id}`, { + avatar_url: filePath, + }); + + const newFullUrl = `https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/public/avatars/${filePath}?t=${new Date().getTime()}`; + setPatientData((prev) => + prev ? { ...prev, avatarFullUrl: newFullUrl } : null + ); + + toast({ + title: "Sucesso!", + description: "Sua foto de perfil foi atualizada.", + }); + } catch (error) { + console.error("Erro no upload do avatar:", error); + toast({ + title: "Erro de Upload", + description: "Não foi possível enviar sua foto.", + variant: "destructive", + }); + } + }; + + if (isAuthLoading || !patientData) { return ( - -
-
-
-

Meus Dados

-

Gerencie suas informações pessoais

-
- -
- -
-
- - - - - Informações Pessoais - - - -
-
- - handleInputChange("name", e.target.value)} disabled={!isEditing} /> -
-
- - handleInputChange("cpf", e.target.value)} disabled={!isEditing} /> -
-
-
- - handleInputChange("birthDate", e.target.value)} disabled={!isEditing} /> -
-
-
- - - - - Contato e Endereço - - - -
-
- - -
-
- - handleInputChange("phone", e.target.value)} disabled={!isEditing} /> -
-
-
-
- - handleInputChange("cep", e.target.value)} disabled={!isEditing} /> -
-
- - handleInputChange("street", e.target.value)} disabled={!isEditing} /> -
-
-
-
- - handleInputChange("number", e.target.value)} disabled={!isEditing} /> -
-
- - handleInputChange("city", e.target.value)} disabled={!isEditing} /> -
-
-
-
-
- -
- - - Resumo do Perfil - - -
-
- - - - {patientData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
- -
- -
-
-

{patientData.name}

-

Paciente

-
-
-
-
- - {patientData.email} -
-
- - {patientData.phone || "Não informado"} -
-
- - {patientData.birthDate ? new Date(patientData.birthDate).toLocaleDateString("pt-BR", { timeZone: "UTC" }) : "Não informado"} -
-
-
-
-
-
-
-
+ +
Carregando seus dados...
+
); + } + + return ( + +
+
+
+

Meus Dados

+

Gerencie suas informações pessoais

+
+ +
+ +
+
+ + + + + Informações Pessoais + + + +
+
+ + + handleInputChange("name", e.target.value) + } + disabled={!isEditing} + /> +
+
+ + handleInputChange("cpf", e.target.value)} + disabled={!isEditing} + /> +
+
+
+ + + handleInputChange("birthDate", e.target.value) + } + disabled={!isEditing} + /> +
+
+
+ + + + + Contato e Endereço + + + +
+
+ + +
+
+ + + handleInputChange("phone", e.target.value) + } + disabled={!isEditing} + /> +
+
+
+
+ + handleInputChange("cep", e.target.value)} + disabled={!isEditing} + /> +
+
+ + + handleInputChange("street", e.target.value) + } + disabled={!isEditing} + /> +
+
+
+
+ + + handleInputChange("number", e.target.value) + } + disabled={!isEditing} + /> +
+
+ + + handleInputChange("city", e.target.value) + } + disabled={!isEditing} + /> +
+
+
+
+
+ +
+ + + Resumo do Perfil + + +
+
+ + + + {patientData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+ +
+ +
+
+

{patientData.name}

+

Paciente

+
+
+
+
+ + {patientData.email} +
+
+ + {patientData.phone || "Não informado"} +
+
+ + + {patientData.birthDate + ? new Date(patientData.birthDate).toLocaleDateString( + "pt-BR", + { timeZone: "UTC" } + ) + : "Não informado"} + +
+
+
+
+
+
+
+
+ ); } diff --git a/app/patient/register/page.tsx b/app/patient/register/page.tsx index 7176abc..d521f2a 100644 --- a/app/patient/register/page.tsx +++ b/app/patient/register/page.tsx @@ -1,7 +1,6 @@ "use client" import type React from "react" - import { useState } from "react" import { useRouter } from "next/navigation" import Link from "next/link" @@ -9,24 +8,24 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Eye, EyeOff, ArrowLeft } from "lucide-react" +import { ArrowLeft, Loader2 } from "lucide-react" +import { useToast } from "@/hooks/use-toast" +import { usersService } from "@/services/usersApi.mjs" // Mantém a importação +import { isValidCPF } from "@/lib/utils" export default function PatientRegister() { - const [showPassword, setShowPassword] = useState(false) - const [showConfirmPassword, setShowConfirmPassword] = useState(false) + // REMOVIDO: Estados para 'showPassword' e 'showConfirmPassword' const [formData, setFormData] = useState({ name: "", email: "", - password: "", - confirmPassword: "", phone: "", cpf: "", birthDate: "", - address: "", + // REMOVIDO: Campos 'password' e 'confirmPassword' }) const [isLoading, setIsLoading] = useState(false) const router = useRouter() + const { toast } = useToast() const handleInputChange = (field: string, value: string) => { setFormData((prev) => ({ @@ -37,22 +36,52 @@ export default function PatientRegister() { const handleRegister = async (e: React.FormEvent) => { e.preventDefault() + setIsLoading(true) - if (formData.password !== formData.confirmPassword) { - alert("As senhas não coincidem!") + // --- VALIDAÇÃO DE CPF --- + if (!isValidCPF(formData.cpf)) { + toast({ + title: "CPF Inválido", + description: "O CPF informado não é válido. Verifique os dígitos.", + variant: "destructive", + }) + setIsLoading(false) return } - setIsLoading(true) + // --- LÓGICA DE REGISTRO COM ENDPOINT PÚBLICO --- + try { + // ALTERADO: Payload ajustado para o endpoint 'register-patient' + const payload = { + email: formData.email.trim().toLowerCase(), + full_name: formData.name, + phone_mobile: formData.phone, // O endpoint espera 'phone_mobile' + cpf: formData.cpf.replace(/\D/g, ''), + birth_date: formData.birthDate, + } - // Simulação de registro - em produção, conectar com API real - setTimeout(() => { - // Salvar dados do usuário no localStorage para simulação - const { confirmPassword, ...userData } = formData - localStorage.setItem("patientData", JSON.stringify(userData)) - router.push("/patient/dashboard") + // ALTERADO: Chamada para a nova função de serviço + await usersService.registerPatient(payload) + + // ALTERADO: Mensagem de sucesso para refletir o fluxo de confirmação por e-mail + toast({ + title: "Cadastro enviado com sucesso!", + description: "Enviamos um link de confirmação para o seu e-mail. Por favor, verifique sua caixa de entrada para ativar sua conta.", + }) + + // Redireciona para a página de login + router.push("/login") + + } catch (error: any) { + console.error("Erro no registro:", error) + toast({ + title: "Erro ao Criar Conta", + description: error.message || "Não foi possível concluir o cadastro. Verifique seus dados e tente novamente.", + variant: "destructive", + }) + } finally { setIsLoading(false) - }, 1000) + } } return ( @@ -67,136 +96,85 @@ export default function PatientRegister() { - Cadastro de Paciente - Preencha seus dados para criar sua conta + Crie sua Conta de Paciente + Preencha seus dados para acessar o portal MedConnect
- + handleInputChange("name", e.target.value)} required + disabled={isLoading} />
- + handleInputChange("cpf", e.target.value)} placeholder="000.000.000-00" required + disabled={isLoading} />
- + handleInputChange("email", e.target.value)} required + disabled={isLoading} />
- + handleInputChange("phone", e.target.value)} placeholder="(11) 99999-9999" required + disabled={isLoading} />
- + handleInputChange("birthDate", e.target.value)} required + disabled={isLoading} />
-
- -