From f0f65ed10e51323b920e0f228ce3657913908b0c Mon Sep 17 00:00:00 2001 From: m1guelmcf Date: Wed, 29 Oct 2025 20:57:33 -0300 Subject: [PATCH 1/9] ajuste --- app/doctor/disponibilidade/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 127b93f..93165e7 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -15,11 +15,12 @@ export default function AvailabilityPage() { const [error, setError] = useState(null); const router = useRouter(); const [isLoading, setIsLoading] = useState(false); - const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + var userInfo; const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; const [modalidadeConsulta, setModalidadeConsulta] = useState(""); useEffect(() => { + userInfo = JSON.parse(localStorage.getItem("user_info") || "{}") const fetchData = async () => { try { const response = await AvailabilityService.list(); From ec640c5564d109bc966ded3520fc2af91b20192c Mon Sep 17 00:00:00 2001 From: m1guelmcf Date: Wed, 29 Oct 2025 21:07:34 -0300 Subject: [PATCH 2/9] ajustes finais --- app/doctor/dashboard/page.tsx | 5 +++-- app/doctor/disponibilidade/excecoes/page.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index bf91156..5f77eaa 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -31,7 +31,7 @@ type Schedule = { }; export default function PatientDashboard() { - const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + var userInfo; const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; //userInfo.id; const [availability, setAvailability] = useState(null); const [exceptions, setExceptions] = useState(null); @@ -52,7 +52,8 @@ export default function PatientDashboard() { saturday: "Sábado", }; - useEffect(() => { + useEffect(() => { + userInfo = JSON.parse(localStorage.getItem("user_info") || "{}") const fetchData = async () => { try { // fetch para disponibilidade diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx index 115ff54..f66e712 100644 --- a/app/doctor/disponibilidade/excecoes/page.tsx +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -18,6 +18,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs"; // IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN import { Calendar } from "@/components/ui/calendar"; import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas +import { userInfo } from "os"; const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; @@ -48,9 +49,13 @@ export default function ExceptionPage() { const router = useRouter(); const [filteredAppointments, setFilteredAppointments] = useState([]); const [isLoading, setIsLoading] = useState(false); - const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + var userInfo; const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; const [tipo, setTipo] = useState(""); + + useEffect (()=>{ + userInfo = JSON.parse(localStorage.getItem("user_info") || "{}") + }) // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) const [bookedDays, setBookedDays] = useState([]); @@ -110,7 +115,7 @@ export default function ExceptionPage() {

Adicione exceções

-

Altere a disponibilidade em casos especiais para o Dr. {userInfo.user_metadata.full_name}

+

Altere a disponibilidade em casos especiais para o Dr. João Silva

From 271aaef2beaaf924a912ed01d4f60c16c04a758b Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Thu, 30 Oct 2025 19:21:56 -0300 Subject: [PATCH 3/9] ajustes --- app/doctor/dashboard/page.tsx | 5 +++-- app/doctor/disponibilidade/excecoes/page.tsx | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index bf91156..a75a8c4 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -31,7 +31,7 @@ type Schedule = { }; export default function PatientDashboard() { - const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + var userInfo; const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; //userInfo.id; const [availability, setAvailability] = useState(null); const [exceptions, setExceptions] = useState(null); @@ -54,6 +54,7 @@ export default function PatientDashboard() { useEffect(() => { const fetchData = async () => { + userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); try { // fetch para disponibilidade const response = await AvailabilityService.list(); @@ -273,7 +274,7 @@ export default function PatientDashboard() {

{date}

- {startTime} - {endTime}
- + {startTime} - {endTime}
-

diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx index 115ff54..fd0639d 100644 --- a/app/doctor/disponibilidade/excecoes/page.tsx +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -18,6 +18,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs"; // IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN import { Calendar } from "@/components/ui/calendar"; import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas +import { userInfo } from "os"; const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; @@ -48,10 +49,14 @@ export default function ExceptionPage() { const router = useRouter(); const [filteredAppointments, setFilteredAppointments] = useState([]); const [isLoading, setIsLoading] = useState(false); - const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + var userInfo; const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; const [tipo, setTipo] = useState(""); + useEffect(() => { + userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + }); + // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) const [bookedDays, setBookedDays] = useState([]); @@ -110,7 +115,7 @@ export default function ExceptionPage() {

Adicione exceções

-

Altere a disponibilidade em casos especiais para o Dr. {userInfo.user_metadata.full_name}

+

Altere a disponibilidade em casos especiais para o Dr. João Silva

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

{date}

-

- {startTime} - {endTime}
- +

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

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

{weekdaysPT[day]}

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

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

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

Sem horário

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

{weekdaysPT[availability.weekday]}

+
+
+ + handleInputChange("start_time", e.target.value)}/> +
+
+ + handleInputChange("end_time", e.target.value)}/> +
+
+
+
+ + handleInputChange("slot_minutes", e.target.value)} name="duracaoConsulta" required className="mt-1" /> +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ ); +} From ce938a7f2c64727aeb55b01f3ac8d1b03da9bd68 Mon Sep 17 00:00:00 2001 From: m1guelmcf Date: Tue, 4 Nov 2025 08:53:45 -0300 Subject: [PATCH 6/9] remocao das barras de persquisas --- components/doctor-layout.tsx | 773 ++++++++++++++++++-------------- components/finance-layout.tsx | 77 ++-- components/manager-layout.tsx | 128 ++++-- components/patient-layout.tsx | 150 ++++--- components/secretary-layout.tsx | 174 +++---- 5 files changed, 759 insertions(+), 543 deletions(-) diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 2dd2300..4415b4f 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -11,352 +11,459 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Search, + Bell, + Calendar, + Clock, + User, + LogOut, + Menu, + X, + Home, + FileText, + ChevronLeft, + ChevronRight, +} from "lucide-react"; interface DoctorData { - id: string; - name: string; - email: string; - phone: string; - cpf: string; - crm: string; - specialty: string; - department: string; - permissions: object; + id: string; + name: string; + email: string; + phone: string; + cpf: string; + crm: string; + specialty: string; + department: string; + permissions: object; } interface PatientLayoutProps { - children: React.ReactNode; + children: React.ReactNode; } export default function DoctorLayout({ children }: PatientLayoutProps) { - const [doctorData, setDoctorData] = useState(null); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [showLogoutDialog, setShowLogoutDialog] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [windowWidth, setWindowWidth] = useState(0); - const isMobile = windowWidth < 1024; - const router = useRouter(); - const pathname = usePathname(); + const [doctorData, setDoctorData] = useState(null); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [windowWidth, setWindowWidth] = useState(0); + const isMobile = windowWidth < 1024; + const router = useRouter(); + const pathname = usePathname(); - useEffect(() => { - const userInfoString = localStorage.getItem("user_info"); - // --- ALTERAÇÃO PRINCIPAL AQUI --- - // Procurando o token no localStorage, onde ele foi realmente salvo. - const token = localStorage.getItem("token"); + useEffect(() => { + const userInfoString = localStorage.getItem("user_info"); + // --- ALTERAÇÃO PRINCIPAL AQUI --- + // Procurando o token no localStorage, onde ele foi realmente salvo. + const token = localStorage.getItem("token"); - if (userInfoString && token) { - const userInfo = JSON.parse(userInfoString); + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); - setDoctorData({ - id: userInfo.id || "", - name: userInfo.user_metadata?.full_name || "Doutor(a)", - email: userInfo.email || "", - specialty: userInfo.user_metadata?.specialty || "Especialidade", - phone: userInfo.phone || "", - cpf: "", - crm: "", - department: "", - permissions: {}, - }); - } else { - // Se não encontrar, aí sim redireciona. - router.push("/login"); - } - }, [router]); - - // O restante do seu código permanece exatamente o mesmo... - useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - handleResize(); - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }, []); - - useEffect(() => { - if (isMobile) { - setSidebarCollapsed(true); - } else { - setSidebarCollapsed(false); - } - }, [isMobile]); - - const handleLogout = () => { - setShowLogoutDialog(true); - }; - - // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- - const confirmLogout = async () => { - try { - // Chama a função centralizada para fazer o logout no servidor - await api.logout(); - } catch (error) { - // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui - } finally { - // A responsabilidade do componente é apenas limpar o estado local e redirecionar - localStorage.removeItem("user_info"); - localStorage.removeItem("token"); - Cookies.remove("access_token"); // Limpeza de segurança - - setShowLogoutDialog(false); - router.push("/"); // Redireciona para a home - } - }; - - const cancelLogout = () => { - setShowLogoutDialog(false); - }; - - const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; - - const menuItems = [ - { - href: "/doctor/dashboard", - icon: Home, - label: "Dashboard", - // Botão para o dashboard do médico - }, - { - href: "/doctor/consultas", - icon: Calendar, - label: "Consultas", - // Botão para página de consultas marcadas do médico atual - }, - { - href: "/doctor/medicos/editorlaudo", - icon: Clock, - label: "Editor de Laudo", - // Botão para página do editor de laudo - }, - { - href: "/doctor/medicos", - icon: User, - label: "Pacientes", - // Botão para a página de visualização de todos os pacientes - }, - { - href: "/doctor/disponibilidade", - icon: Calendar, - label: "Disponibilidade", - // Botão para o dashboard do médico - }, - ]; - - if (!doctorData) { - return
Carregando...
; + setDoctorData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Doutor(a)", + email: userInfo.email || "", + specialty: userInfo.user_metadata?.specialty || "Especialidade", + phone: userInfo.phone || "", + cpf: "", + crm: "", + department: "", + permissions: {}, + }); + } else { + // Se não encontrar, aí sim redireciona. + router.push("/login"); } + }, [router]); - return ( - // O restante do seu código JSX permanece exatamente o mesmo -
-
-
-
- {!sidebarCollapsed && ( -
-
-
-
- MediConnect -
- )} - -
+ // O restante do seu código permanece exatamente o mesmo... + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + useEffect(() => { + if (isMobile) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } + }, [isMobile]); + + const handleLogout = () => { + setShowLogoutDialog(true); + }; + + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home + } + }; + + const cancelLogout = () => { + setShowLogoutDialog(false); + }; + + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + + const menuItems = [ + { + href: "/doctor/dashboard", + icon: Home, + label: "Dashboard", + // Botão para o dashboard do médico + }, + { + href: "/doctor/consultas", + icon: Calendar, + label: "Consultas", + // Botão para página de consultas marcadas do médico atual + }, + { + href: "/doctor/medicos/editorlaudo", + icon: Clock, + label: "Editor de Laudo", + // Botão para página do editor de laudo + }, + { + href: "/doctor/medicos", + icon: User, + label: "Pacientes", + // Botão para a página de visualização de todos os pacientes + }, + { + href: "/doctor/disponibilidade", + icon: Calendar, + label: "Disponibilidade", + // Botão para o dashboard do médico + }, + ]; + + if (!doctorData) { + return
Carregando...
; + } + + return ( + // O restante do seu código JSX permanece exatamente o mesmo +
+
+
+
+ {!sidebarCollapsed && ( +
+
+
- - // ... (seu código anterior) - {/* Sidebar para desktop */} -
-
-
- {!sidebarCollapsed && ( -
-
-
-
- MediConnect -
- )} - -
-
- - - -
-
- {!sidebarCollapsed && ( - <> - - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-

{doctorData.name}

-

{doctorData.specialty}

-
- - )} - {sidebarCollapsed && ( - - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - - )} -
- -
- - {!sidebarCollapsed && Sair} -
-
-
-
- {isMobileMenuOpen &&
} -
-
-
-
-
-
- Hospital System -
- -
- - - -
-
- - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - -
-

{doctorData.name}

-

{doctorData.specialty}

-
-
- -
-
- -
-
-
-
-
- - -
-
- -
- -
-
-
- -
{children}
-
- - - - - Confirmar Saída - Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta. - - - - - - - + MediConnect +
+ )} + +
- ); + + // ... (seu código anterior) + {/* Sidebar para desktop */} +
+
+
+ {!sidebarCollapsed && ( +
+
+
+
+ + MediConnect + +
+ )} + +
+
+ + + +
+
+ {!sidebarCollapsed && ( + <> + + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

+ {doctorData.name} +

+

+ {doctorData.specialty} +

+
+ + )} + {sidebarCollapsed && ( + + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + + )} +
+ +
+ + {!sidebarCollapsed && Sair} +
+
+
+
+ {isMobileMenuOpen && ( +
+ )} +
+
+
+
+
+
+ Hospital System +
+ +
+ + + +
+
+ + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

+ {doctorData.name} +

+

+ {doctorData.specialty} +

+
+
+ +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
{children}
+
+ + + + + Confirmar Saída + + Deseja realmente sair do sistema? Você precisará fazer login + novamente para acessar sua conta. + + + + + + + + +
+ ); } diff --git a/components/finance-layout.tsx b/components/finance-layout.tsx index d1904b7..5f1b07b 100644 --- a/components/finance-layout.tsx +++ b/components/finance-layout.tsx @@ -6,14 +6,34 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; -import { api } from '@/services/api.mjs'; +import { api } from "@/services/api.mjs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Search, + Bell, + Calendar, + Clock, + User, + LogOut, + Menu, + X, + Home, + FileText, + ChevronLeft, + ChevronRight, +} from "lucide-react"; interface FinancierData { id: string; @@ -30,7 +50,9 @@ interface PatientLayoutProps { } export default function FinancierLayout({ children }: PatientLayoutProps) { - const [financierData, setFinancierData] = useState(null); + const [financierData, setFinancierData] = useState( + null + ); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const router = useRouter(); @@ -43,12 +65,13 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - + setFinancierData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Financeiro", email: userInfo.email || "", - department: userInfo.user_metadata?.department || "Departamento Financeiro", + department: + userInfo.user_metadata?.department || "Departamento Financeiro", phone: userInfo.phone || "", cpf: "", permissions: {}, @@ -79,18 +102,18 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- const confirmLogout = async () => { try { - // Chama a função centralizada para fazer o logout no servidor - await api.logout(); + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); } catch (error) { - // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui } finally { - // A responsabilidade do componente é apenas limpar o estado local e redirecionar - localStorage.removeItem("user_info"); - localStorage.removeItem("token"); - Cookies.remove("access_token"); // Limpeza de segurança - - setShowLogoutDialog(false); - router.push("/"); // Redireciona para a home + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home } }; @@ -106,7 +129,11 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { ]; if (!financierData) { - return
Carregando...
; + return ( +
+ Carregando... +
+ ); } return ( @@ -202,9 +229,7 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { } onClick={handleLogout} > - + {!sidebarCollapsed && "Sair"}
@@ -217,15 +242,7 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { >
-
-
- - -
-
+
); -} \ No newline at end of file +} diff --git a/components/manager-layout.tsx b/components/manager-layout.tsx index 7c1e3fe..40cf0df 100644 --- a/components/manager-layout.tsx +++ b/components/manager-layout.tsx @@ -6,14 +6,30 @@ import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout -import { api } from '@/services/api.mjs'; +import { api } from "@/services/api.mjs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Search, + Bell, + Calendar, + User, + LogOut, + ChevronLeft, + ChevronRight, + Home, +} from "lucide-react"; interface ManagerData { id: string; @@ -43,7 +59,7 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - + setManagerData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Gestor(a)", @@ -77,18 +93,18 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- const confirmLogout = async () => { try { - // Chama a função centralizada para fazer o logout no servidor - await api.logout(); + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); } catch (error) { - // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui } finally { - // A responsabilidade do componente é apenas limpar o estado local e redirecionar - localStorage.removeItem("user_info"); - localStorage.removeItem("token"); - Cookies.remove("access_token"); // Limpeza de segurança - - setShowLogoutDialog(false); - router.push("/"); // Redireciona para a home + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home } }; @@ -103,13 +119,19 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { ]; if (!managerData) { - return
Carregando...
; + return ( +
+ Carregando... +
+ ); } return (
{!sidebarCollapsed && ( @@ -117,9 +139,7 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
- - MediConnect - + MediConnect
)}
@@ -139,10 +163,16 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { return (
- {!sidebarCollapsed && {item.label}} + {!sidebarCollapsed && ( + {item.label} + )}
); @@ -153,19 +183,32 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
- {managerData.name.split(" ").map((n) => n[0]).join("")} + + {managerData.name + .split(" ") + .map((n) => n[0]) + .join("")} + {!sidebarCollapsed && (
-

{managerData.name}

-

{managerData.department}

+

+ {managerData.name} +

+

+ {managerData.department} +

)}
-
+
-
-
- - -
-
+
@@ -196,14 +240,22 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { Confirmar Saída - Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta. + + Deseja realmente sair do sistema? Você precisará fazer login + novamente para acessar sua conta. + - - + +
); -} \ No newline at end of file +} diff --git a/components/patient-layout.tsx b/components/patient-layout.tsx index 7ac9415..a7cc9fb 100644 --- a/components/patient-layout.tsx +++ b/components/patient-layout.tsx @@ -1,52 +1,70 @@ -"use client" +"use client"; import Cookies from "js-cookie"; -import type React from "react" -import { useState, useEffect } from "react" -import Link from "next/link" -import { useRouter, usePathname } from "next/navigation" +import type React from "react"; +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { useRouter, usePathname } from "next/navigation"; import { api } from "@/services/api.mjs"; // Importando nosso cliente de API -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Badge } from "@/components/ui/badge" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react" -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Search, + Bell, + User, + LogOut, + FileText, + Clock, + Calendar, + Home, + ChevronLeft, + ChevronRight, +} from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; interface PatientData { - name: string - email: string - phone: string - cpf: string - birthDate: string - address: string + name: string; + email: string; + phone: string; + cpf: string; + birthDate: string; + address: string; } interface PatientLayoutProps { - children: React.ReactNode + children: React.ReactNode; } // --- ALTERAÇÃO 1: Renomeando o componente para maior clareza --- export default function PatientLayout({ children }: PatientLayoutProps) { - const [patientData, setPatientData] = useState(null) - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) - const [showLogoutDialog, setShowLogoutDialog] = useState(false) - const router = useRouter() - const pathname = usePathname() + const [patientData, setPatientData] = useState(null); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const router = useRouter(); + const pathname = usePathname(); useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true) + setSidebarCollapsed(true); } else { - setSidebarCollapsed(false) + setSidebarCollapsed(false); } - } - handleResize() - window.addEventListener("resize", handleResize) - return () => window.removeEventListener("resize", handleResize) - }, []) + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); useEffect(() => { const userInfoString = localStorage.getItem("user_info"); @@ -55,12 +73,12 @@ export default function PatientLayout({ children }: PatientLayoutProps) { if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - + setPatientData({ name: userInfo.user_metadata?.full_name || "Paciente", email: userInfo.email || "", phone: userInfo.phone || "", - cpf: "", + cpf: "", birthDate: "", address: "", }); @@ -69,39 +87,47 @@ export default function PatientLayout({ children }: PatientLayoutProps) { router.push("/login"); } }, [router]); - - const handleLogout = () => setShowLogoutDialog(true) + + const handleLogout = () => setShowLogoutDialog(true); // --- ALTERAÇÃO 4: Função de logout completa e padronizada --- const confirmLogout = async () => { try { - // Chama a função centralizada para fazer o logout no servidor - await api.logout(); + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); } catch (error) { - console.error("Erro ao tentar fazer logout no servidor:", error); + console.error("Erro ao tentar fazer logout no servidor:", error); } finally { - // Limpeza completa e consistente do estado local - localStorage.removeItem("user_info"); - localStorage.removeItem("token"); - Cookies.remove("access_token"); // Limpeza de segurança - - setShowLogoutDialog(false); - router.push("/"); // Redireciona para a página inicial + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial } }; - const cancelLogout = () => setShowLogoutDialog(false) + const cancelLogout = () => setShowLogoutDialog(false); const menuItems = [ { href: "/patient/dashboard", icon: Home, label: "Dashboard" }, - { href: "/patient/appointments", icon: Calendar, label: "Minhas Consultas" }, + { + href: "/patient/appointments", + icon: Calendar, + label: "Minhas Consultas", + }, { href: "/patient/schedule", icon: Clock, label: "Agendar Consulta" }, { href: "/patient/reports", icon: FileText, label: "Meus Laudos" }, { href: "/patient/profile", icon: User, label: "Meus Dados" }, - ] + ]; if (!patientData) { - return
Carregando...
; + return ( +
+ Carregando... +
+ ); } return ( @@ -120,7 +146,9 @@ export default function PatientLayout({ children }: PatientLayoutProps) {
- MediConnect + + MediConnect +
)}
- ) + ); })} @@ -199,9 +227,7 @@ export default function PatientLayout({ children }: PatientLayoutProps) { } onClick={handleLogout} > - {" "} + {" "} {/* Remove margem quando colapsado */} {!sidebarCollapsed && "Sair"}{" "} {/* Mostra o texto apenas quando não está colapsado */} @@ -218,15 +244,7 @@ export default function PatientLayout({ children }: PatientLayoutProps) { {/* Header */}
-
-
- - -
-
+
- ) -} \ No newline at end of file + ); +} diff --git a/components/secretary-layout.tsx b/components/secretary-layout.tsx index 5bd51fd..edfc4b4 100644 --- a/components/secretary-layout.tsx +++ b/components/secretary-layout.tsx @@ -1,41 +1,60 @@ // Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo) -"use client" +"use client"; -import type React from "react" -import { useState, useEffect } from "react" -import { useRouter, usePathname } from "next/navigation" -import Link from "next/link" +import type React from "react"; +import { useState, useEffect } from "react"; +import { useRouter, usePathname } from "next/navigation"; +import Link from "next/link"; import Cookies from "js-cookie"; -import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central +import { api } from "@/services/api.mjs"; // Importando nosso cliente de API central -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Badge } from "@/components/ui/badge" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react" +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Search, + Bell, + Calendar, + Clock, + User, + LogOut, + Home, + ChevronLeft, + ChevronRight, +} from "lucide-react"; interface SecretaryData { - id: string - name: string - email: string - phone: string - cpf: string - employeeId: string - department: string - permissions: object + id: string; + name: string; + email: string; + phone: string; + cpf: string; + employeeId: string; + department: string; + permissions: object; } interface SecretaryLayoutProps { - children: React.ReactNode + children: React.ReactNode; } export default function SecretaryLayout({ children }: SecretaryLayoutProps) { - const [secretaryData, setSecretaryData] = useState(null); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false) - const [showLogoutDialog, setShowLogoutDialog] = useState(false) - const router = useRouter() - const pathname = usePathname() + const [secretaryData, setSecretaryData] = useState( + null + ); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const router = useRouter(); + const pathname = usePathname(); useEffect(() => { const userInfoString = localStorage.getItem("user_info"); @@ -44,7 +63,7 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - + setSecretaryData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Secretária", @@ -64,50 +83,53 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true) + setSidebarCollapsed(true); } else { - setSidebarCollapsed(false) + setSidebarCollapsed(false); } - } - handleResize() - window.addEventListener("resize", handleResize) - return () => window.removeEventListener("resize", handleResize) - }, []) + }; + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); - const handleLogout = () => setShowLogoutDialog(true) + const handleLogout = () => setShowLogoutDialog(true); // --- ALTERAÇÃO 3: Função de logout completa e padronizada --- const confirmLogout = async () => { try { - // Chama a função centralizada para fazer o logout no servidor - await api.logout(); + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); } catch (error) { - console.error("Erro ao tentar fazer logout no servidor:", error); + console.error("Erro ao tentar fazer logout no servidor:", error); } finally { - // Limpeza completa e consistente do estado local - localStorage.removeItem("user_info"); - localStorage.removeItem("token"); - Cookies.remove("access_token"); // Limpeza de segurança - - setShowLogoutDialog(false); - router.push("/"); // Redireciona para a página inicial + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial } }; - const cancelLogout = () => setShowLogoutDialog(false) + const cancelLogout = () => setShowLogoutDialog(false); const menuItems = [ { href: "/secretary/dashboard", icon: Home, label: "Dashboard" }, { href: "/secretary/appointments", icon: Calendar, label: "Consultas" }, { href: "/secretary/schedule", icon: Clock, label: "Agendar Consulta" }, { href: "/secretary/pacientes", icon: User, label: "Pacientes" }, - ] + ]; if (!secretaryData) { - return
Carregando...
; + return ( +
+ Carregando... +
+ ); } - return (
{/* Sidebar */} @@ -123,7 +145,9 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
- MediConnect + + MediConnect +
)}
)}
@@ -193,9 +223,7 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { } onClick={handleLogout} > - + {!sidebarCollapsed && "Sair"}
@@ -203,20 +231,13 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { {/* Main Content */}
-
-
- - -
-
+
- ) -} \ No newline at end of file + ); +} From 63fc99c15156d65d92bd167f2fec0613a838d983 Mon Sep 17 00:00:00 2001 From: m1guelmcf Date: Tue, 4 Nov 2025 09:12:45 -0300 Subject: [PATCH 7/9] ajuste do filtro de paciente --- app/secretary/pacientes/page.tsx | 804 +++++++++++++++++-------------- 1 file changed, 439 insertions(+), 365 deletions(-) diff --git a/app/secretary/pacientes/page.tsx b/app/secretary/pacientes/page.tsx index 3715261..ca02124 100644 --- a/app/secretary/pacientes/page.tsx +++ b/app/secretary/pacientes/page.tsx @@ -3,391 +3,465 @@ import { useState, useEffect, useRef, 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 { Plus, Edit, Trash2, Eye, Calendar, Filter } 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 SecretaryLayout from "@/components/secretary-layout"; import { patientsService } from "@/services/patientsApi.mjs"; export default function PacientesPage() { - const [searchTerm, setSearchTerm] = useState(""); - const [convenioFilter, setConvenioFilter] = useState("all"); - const [vipFilter, setVipFilter] = useState("all"); - const [patients, setPatients] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [page, setPage] = useState(1); - const [hasNext, setHasNext] = useState(true); - const [isFetching, setIsFetching] = useState(false); - const observerRef = useRef(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [patientToDelete, setPatientToDelete] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [patientDetails, setPatientDetails] = useState(null); - const openDetailsDialog = async (patientId: string) => { - setDetailsDialogOpen(true); - setPatientDetails(null); - try { - const res = await patientsService.getById(patientId); - setPatientDetails(res[0]); - } catch (e: any) { - setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); - } - }; + const [searchTerm, setSearchTerm] = useState(""); + const [convenioFilter, setConvenioFilter] = useState("all"); + const [vipFilter, setVipFilter] = useState("all"); + const [patients, setPatients] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const [hasNext, setHasNext] = useState(true); + const [isFetching, setIsFetching] = useState(false); + const observerRef = useRef(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [patientDetails, setPatientDetails] = useState(null); + const openDetailsDialog = async (patientId: string) => { + setDetailsDialogOpen(true); + setPatientDetails(null); + try { + const res = await patientsService.getById(patientId); + setPatientDetails(res[0]); + } catch (e: any) { + setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); + } + }; - const fetchPacientes = useCallback( - async (pageToFetch: number) => { - if (isFetching || !hasNext) return; - setIsFetching(true); - setError(null); - try { - 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 ?? "", - ultimoAtendimento: p.last_visit_at ?? "", - proximoAtendimento: p.next_appointment_at ?? "", - vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "", // se não existir, fica vazio - status: p.status ?? undefined, - })); + const fetchPacientes = useCallback( + async (pageToFetch: number) => { + if (isFetching || !hasNext) return; + setIsFetching(true); + setError(null); + try { + 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 ?? "", + ultimoAtendimento: p.last_visit_at ?? "", + proximoAtendimento: p.next_appointment_at ?? "", + vip: Boolean(p.vip ?? false), + convenio: p.convenio ?? "", // se não existir, fica vazio + status: p.status ?? undefined, + })); - setPatients((prev) => { - const all = [...prev, ...mapped]; - const unique = Array.from(new Map(all.map((p) => [p.id, p])).values()); - return unique; - }); - - if (!mapped.id) setHasNext(false); // parar carregamento - else setPage((prev) => prev + 1); - } catch (e: any) { - setError(e?.message || "Erro ao buscar pacientes"); - } finally { - setIsFetching(false); - } - }, - [isFetching, hasNext] - ); - - useEffect(() => { - fetchPacientes(page); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!observerRef.current || !hasNext) return; - const observer = new window.IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isFetching && hasNext) { - fetchPacientes(page); - } + setPatients((prev) => { + const all = [...prev, ...mapped]; + const unique = Array.from( + new Map(all.map((p) => [p.id, p])).values() + ); + return unique; }); - observer.observe(observerRef.current); - return () => { - if (observerRef.current) observer.unobserve(observerRef.current); - }; - }, [fetchPacientes, page, hasNext, isFetching]); - const handleDeletePatient = async (patientId: string) => { - // Remove from current list (client-side deletion) - try { - const res = await patientsService.delete(patientId); + if (!mapped.id) setHasNext(false); // parar carregamento + else setPage((prev) => prev + 1); + } catch (e: any) { + setError(e?.message || "Erro ao buscar pacientes"); + } finally { + setIsFetching(false); + } + }, + [isFetching, hasNext] + ); - if (res) { - alert(`${res.error} ${res.message}`); - } + useEffect(() => { + fetchPacientes(page); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - setPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId))); - } catch (e: any) { - setError(e?.message || "Erro ao deletar paciente"); - } - setDeleteDialogOpen(false); - setPatientToDelete(null); - }; - - const openDeleteDialog = (patientId: string) => { - setPatientToDelete(patientId); - setDeleteDialogOpen(true); - }; - - const filteredPatients = patients.filter((patient) => { - const matchesSearch = patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || patient.telefone?.includes(searchTerm); - const matchesConvenio = convenioFilter === "all" || (patient.convenio ?? "") === convenioFilter; - const matchesVip = vipFilter === "all" || (vipFilter === "vip" && patient.vip) || (vipFilter === "regular" && !patient.vip); - - return matchesSearch && matchesConvenio && matchesVip; + useEffect(() => { + if (!observerRef.current || !hasNext) return; + const observer = new window.IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !isFetching && hasNext) { + fetchPacientes(page); + } }); + observer.observe(observerRef.current); + return () => { + if (observerRef.current) observer.unobserve(observerRef.current); + }; + }, [fetchPacientes, page, hasNext, isFetching]); - return ( - -
-
-
-

Pacientes

-

Gerencie as informações de seus pacientes

-
-
- - - -
-
+ const handleDeletePatient = async (patientId: string) => { + // Remove from current list (client-side deletion) + try { + const res = await patientsService.delete(patientId); -
- {/* Convênio */} -
- Convênio - -
+ if (res) { + alert(`${res.error} ${res.message}`); + } -
- VIP - -
-
- Aniversariantes - -
+ setPatients((prev) => + prev.filter((p) => String(p.id) !== String(patientId)) + ); + } catch (e: any) { + setError(e?.message || "Erro ao deletar paciente"); + } + setDeleteDialogOpen(false); + setPatientToDelete(null); + }; -
- VIP - -
+ const openDeleteDialog = (patientId: string) => { + setPatientToDelete(patientId); + setDeleteDialogOpen(true); + }; -
- Aniversariantes - -
+ const filteredPatients = patients.filter((patient) => { + const matchesSearch = + patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || + patient.telefone?.includes(searchTerm); + const matchesConvenio = + convenioFilter === "all" || (patient.convenio ?? "") === convenioFilter; + const matchesVip = + vipFilter === "all" || + (vipFilter === "vip" && patient.vip) || + (vipFilter === "regular" && !patient.vip); - -
+ return matchesSearch && matchesConvenio && matchesVip; + }); -
-
- {error ? ( -
{`Erro ao carregar pacientes: ${error}`}
- ) : ( - - - - - - - - - - - - - - {filteredPatients.length === 0 ? ( - - - - ) : ( - filteredPatients.map((patient) => ( - - - - - - - - - - )) - )} - -
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAções
- {patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
-
-
- {patient.nome?.charAt(0) || "?"} -
- {patient.nome} -
-
{patient.telefone}{patient.cidade}{patient.estado}{patient.ultimoAtendimento}{patient.proximoAtendimento} - - -
Ações
-
- - openDetailsDialog(String(patient.id))}> - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(String(patient.id))}> - - Excluir - - -
-
- )} -
- {isFetching &&
Carregando mais pacientes...
} -
-
+ return ( + +
+
+
+

+ Pacientes +

+

+ Gerencie as informações de seus pacientes +

+
+
+ + + +
+
- - - - 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"> +
+ {/* Convênio */} +
+ + Convênio + + +
+ +
+ VIP + +
+
+ + Aniversariantes + + +
+ + +
+ +
+
+ {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : ( + + + + + + + + + + + + + + {filteredPatients.length === 0 ? ( + + + + ) : ( + filteredPatients.map((patient) => ( + + + + + + + + + + )) + )} + +
+ Nome + + Telefone + + Cidade + + Estado + + Último atendimento + + Próximo atendimento + + Ações +
+ {patients.length === 0 + ? "Nenhum paciente cadastrado" + : "Nenhum paciente encontrado com os filtros aplicados"} +
+
+
+ + {patient.nome?.charAt(0) || "?"} + +
+ + {patient.nome} + +
+
+ {patient.telefone} + {patient.cidade}{patient.estado} + {patient.ultimoAtendimento} + + {patient.proximoAtendimento} + + + +
Ações
+
+ + + openDetailsDialog(String(patient.id)) + } + > + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + + openDeleteDialog(String(patient.id)) + } + > + Excluir - - - - + + +
+
+ )} +
+ {isFetching && ( +
+ Carregando mais pacientes... +
+ )} +
+
- {/* Modal de detalhes do paciente */} - - - - Detalhes do Paciente - - {patientDetails === null ? ( -
Carregando...
- ) : patientDetails?.error ? ( -
{patientDetails.error}
- ) : ( -
-

- Nome: {patientDetails.full_name} -

-

- CPF: {patientDetails.cpf} -

-

- Email: {patientDetails.email} -

-

- Telefone: {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"} -

-

- Nome social: {patientDetails.social_name ?? "-"} -

-

- Sexo: {patientDetails.sex ?? "-"} -

-

- Tipo sanguíneo: {patientDetails.blood_type ?? "-"} -

-

- Peso: {patientDetails.weight_kg ?? "-"} - {patientDetails.weight_kg ? "kg" : ""} -

-

- Altura: {patientDetails.height_m ?? "-"} - {patientDetails.height_m ? "m" : ""} -

-

- IMC: {patientDetails.bmi ?? "-"} -

-

- Endereço: {patientDetails.street ?? "-"} -

-

- Bairro: {patientDetails.neighborhood ?? "-"} -

-

- Cidade: {patientDetails.city ?? "-"} -

-

- Estado: {patientDetails.state ?? "-"} -

-

- CEP: {patientDetails.cep ?? "-"} -

-

- Criado em: {patientDetails.created_at ?? "-"} -

-

- Atualizado em: {patientDetails.updated_at ?? "-"} -

-

- Id: {patientDetails.id ?? "-"} -

-
- )} -
-
- - Fechar - -
-
-
- - ); + + + + 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 + + + + + + {/* Modal de detalhes do paciente */} + + + + Detalhes do Paciente + + {patientDetails === null ? ( +
Carregando...
+ ) : patientDetails?.error ? ( +
{patientDetails.error}
+ ) : ( +
+

+ Nome: {patientDetails.full_name} +

+

+ CPF: {patientDetails.cpf} +

+

+ Email: {patientDetails.email} +

+

+ Telefone:{" "} + {patientDetails.phone_mobile ?? + patientDetails.phone1 ?? + patientDetails.phone2 ?? + "-"} +

+

+ Nome social:{" "} + {patientDetails.social_name ?? "-"} +

+

+ Sexo: {patientDetails.sex ?? "-"} +

+

+ Tipo sanguíneo:{" "} + {patientDetails.blood_type ?? "-"} +

+

+ Peso: {patientDetails.weight_kg ?? "-"} + {patientDetails.weight_kg ? "kg" : ""} +

+

+ Altura: {patientDetails.height_m ?? "-"} + {patientDetails.height_m ? "m" : ""} +

+

+ IMC: {patientDetails.bmi ?? "-"} +

+

+ Endereço: {patientDetails.street ?? "-"} +

+

+ Bairro:{" "} + {patientDetails.neighborhood ?? "-"} +

+

+ Cidade: {patientDetails.city ?? "-"} +

+

+ Estado: {patientDetails.state ?? "-"} +

+

+ CEP: {patientDetails.cep ?? "-"} +

+

+ Criado em:{" "} + {patientDetails.created_at ?? "-"} +

+

+ Atualizado em:{" "} + {patientDetails.updated_at ?? "-"} +

+

+ Id: {patientDetails.id ?? "-"} +

+
+ )} +
+
+ + Fechar + +
+
+
+
+ ); } From 66212930e8c5f1fa70ac075eb4793da071a13004 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Tue, 4 Nov 2025 14:26:45 -0300 Subject: [PATCH 8/9] =?UTF-8?q?Adicionado=20gest=C3=A3o=20de=20pacientes?= =?UTF-8?q?=20para=20o=20gestor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/manager/pacientes/[id]/editar/page.tsx | 682 +++++++++++++++++++++ app/manager/pacientes/loading.tsx | 3 + app/manager/pacientes/novo/page.tsx | 676 ++++++++++++++++++++ app/manager/pacientes/page.tsx | 393 ++++++++++++ components/manager-layout.tsx | 1 + 5 files changed, 1755 insertions(+) create mode 100644 app/manager/pacientes/[id]/editar/page.tsx create mode 100644 app/manager/pacientes/loading.tsx create mode 100644 app/manager/pacientes/novo/page.tsx create mode 100644 app/manager/pacientes/page.tsx diff --git a/app/manager/pacientes/[id]/editar/page.tsx b/app/manager/pacientes/[id]/editar/page.tsx new file mode 100644 index 0000000..254be97 --- /dev/null +++ b/app/manager/pacientes/[id]/editar/page.tsx @@ -0,0 +1,682 @@ +"use client"; + +import type React from "react"; + +import { useState, useEffect, useRef } from "react"; +import { useRouter, useParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ArrowLeft, Save, Trash2, Paperclip, Upload } from "lucide-react"; +import Link from "next/link"; +import { useToast } from "@/hooks/use-toast"; +import SecretaryLayout from "@/components/secretary-layout"; +import { patientsService } from "@/services/patientsApi.mjs"; +import { json } from "stream/consumers"; + +export default function EditarPacientePage() { + const router = useRouter(); + const params = useParams(); + const patientId = params.id; + const { toast } = useToast(); + + // Photo upload state + const fileInputRef = useRef(null); + const [isUploadingPhoto, setIsUploadingPhoto] = useState(false); + const [photoUrl, setPhotoUrl] = useState(null); + // Anexos state + const [anexos, setAnexos] = useState([]); + const [isUploadingAnexo, setIsUploadingAnexo] = useState(false); + const anexoInputRef = useRef(null); + + type FormData = { + nome: string; // full_name + cpf: string; + dataNascimento: string; // birth_date + sexo: string; // sex + id?: string; + nomeSocial?: string; // social_name + rg?: string; + documentType?: string; // document_type + documentNumber?: string; // document_number + ethnicity?: string; + race?: string; + naturality?: string; + nationality?: string; + profession?: string; + maritalStatus?: string; // marital_status + motherName?: string; // mother_name + motherProfession?: string; // mother_profession + fatherName?: string; // father_name + fatherProfession?: string; // father_profession + guardianName?: string; // guardian_name + guardianCpf?: string; // guardian_cpf + spouseName?: string; // spouse_name + rnInInsurance?: boolean; // rn_in_insurance + legacyCode?: string; // legacy_code + notes?: string; + email?: string; + phoneMobile?: string; // phone_mobile + phone1?: string; + phone2?: string; + cep?: string; + street?: string; + number?: string; + complement?: string; + neighborhood?: string; + city?: string; + state?: string; + reference?: string; + vip?: boolean; + lastVisitAt?: string; + nextAppointmentAt?: string; + createdAt?: string; + updatedAt?: string; + createdBy?: string; + updatedBy?: string; + weightKg?: string; + heightM?: string; + bmi?: string; + bloodType?: string; + }; + + + const [formData, setFormData] = useState({ + nome: "", + cpf: "", + dataNascimento: "", + sexo: "", + id: "", + nomeSocial: "", + rg: "", + documentType: "", + documentNumber: "", + ethnicity: "", + race: "", + naturality: "", + nationality: "", + profession: "", + maritalStatus: "", + motherName: "", + motherProfession: "", + fatherName: "", + fatherProfession: "", + guardianName: "", + guardianCpf: "", + spouseName: "", + rnInInsurance: false, + legacyCode: "", + notes: "", + email: "", + phoneMobile: "", + phone1: "", + phone2: "", + cep: "", + street: "", + number: "", + complement: "", + neighborhood: "", + city: "", + state: "", + reference: "", + vip: false, + lastVisitAt: "", + nextAppointmentAt: "", + createdAt: "", + updatedAt: "", + createdBy: "", + updatedBy: "", + weightKg: "", + heightM: "", + bmi: "", + bloodType: "", + }); + + const [isGuiaConvenio, setIsGuiaConvenio] = useState(false); + const [validadeIndeterminada, setValidadeIndeterminada] = useState(false); + + useEffect(() => { + async function fetchPatient() { + try { + const res = await patientsService.getById(patientId); + // Map API snake_case/nested to local camelCase form + setFormData({ + id: res[0]?.id ?? "", + nome: res[0]?.full_name ?? "", + nomeSocial: res[0]?.social_name ?? "", + cpf: res[0]?.cpf ?? "", + rg: res[0]?.rg ?? "", + documentType: res[0]?.document_type ?? "", + documentNumber: res[0]?.document_number ?? "", + sexo: res[0]?.sex ?? "", + dataNascimento: res[0]?.birth_date ?? "", + ethnicity: res[0]?.ethnicity ?? "", + race: res[0]?.race ?? "", + naturality: res[0]?.naturality ?? "", + nationality: res[0]?.nationality ?? "", + profession: res[0]?.profession ?? "", + maritalStatus: res[0]?.marital_status ?? "", + motherName: res[0]?.mother_name ?? "", + motherProfession: res[0]?.mother_profession ?? "", + fatherName: res[0]?.father_name ?? "", + fatherProfession: res[0]?.father_profession ?? "", + guardianName: res[0]?.guardian_name ?? "", + guardianCpf: res[0]?.guardian_cpf ?? "", + spouseName: res[0]?.spouse_name ?? "", + rnInInsurance: res[0]?.rn_in_insurance ?? false, + legacyCode: res[0]?.legacy_code ?? "", + notes: res[0]?.notes ?? "", + email: res[0]?.email ?? "", + phoneMobile: res[0]?.phone_mobile ?? "", + phone1: res[0]?.phone1 ?? "", + phone2: res[0]?.phone2 ?? "", + cep: res[0]?.cep ?? "", + street: res[0]?.street ?? "", + number: res[0]?.number ?? "", + complement: res[0]?.complement ?? "", + neighborhood: res[0]?.neighborhood ?? "", + city: res[0]?.city ?? "", + state: res[0]?.state ?? "", + reference: res[0]?.reference ?? "", + vip: res[0]?.vip ?? false, + lastVisitAt: res[0]?.last_visit_at ?? "", + nextAppointmentAt: res[0]?.next_appointment_at ?? "", + createdAt: res[0]?.created_at ?? "", + updatedAt: res[0]?.updated_at ?? "", + createdBy: res[0]?.created_by ?? "", + updatedBy: res[0]?.updated_by ?? "", + weightKg: res[0]?.weight_kg ? String(res[0].weight_kg) : "", + heightM: res[0]?.height_m ? String(res[0].height_m) : "", + bmi: res[0]?.bmi ? String(res[0].bmi) : "", + bloodType: res[0]?.blood_type ?? "", + }); + + } catch (e: any) { + toast({ title: "Erro", description: e?.message || "Falha ao carregar paciente" }); + } + } + fetchPatient(); + }, [patientId, toast]); + + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + // Build API payload (snake_case) + const payload = { + full_name: formData.nome || null, + cpf: formData.cpf || null, + email: formData.email || null, + phone_mobile: formData.phoneMobile || null, + birth_date: formData.dataNascimento || null, + social_name: formData.nomeSocial || null, + sex: formData.sexo || null, + blood_type: formData.bloodType || null, + weight_kg: formData.weightKg ? Number(formData.weightKg) : null, + height_m: formData.heightM ? Number(formData.heightM) : null, + street: formData.street || null, + number: formData.number || null, + complement: formData.complement || null, + neighborhood: formData.neighborhood || null, + city: formData.city || null, + state: formData.state || null, + cep: formData.cep || null, + }; + + try { + await patientsService.update(patientId, payload); + toast({ + title: "Sucesso", + description: "Paciente atualizado com sucesso", + variant: "default" + }); + router.push("/manager/pacientes"); + } catch (err: any) { + console.error("Erro ao atualizar paciente:", err); + toast({ + title: "Erro", + description: err?.message || "Não foi possível atualizar o paciente", + variant: "destructive" + }); + } + }; + + return ( + +
+
+ + + +
+

Editar Paciente

+

Atualize as informações do paciente

+
+ + {/* Anexos Section */} +
+

Anexos

+
+ + +
+ {anexos.length === 0 ? ( +

Nenhum anexo encontrado.

+ ) : ( +
    + {anexos.map((a) => ( +
  • +
    + + {a.nome || a.filename || `Anexo ${a.id}`} +
    + +
  • + ))} +
+ )} +
+
+ +
+
+

Dados Pessoais

+ +
+ {/* Photo upload */} +
+ +
+
+ {photoUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + Foto do paciente + ) : ( + Sem foto + )} +
+
+ + + {photoUrl && ( + + )} +
+
+
+
+ + handleInputChange("nome", e.target.value)} required /> +
+ +
+ + handleInputChange("cpf", e.target.value)} placeholder="000.000.000-00" required /> +
+ +
+ + handleInputChange("rg", e.target.value)} placeholder="00.000.000-0" /> +
+ +
+ +
+
+ handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" /> + +
+
+ handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" /> + +
+
+
+ +
+ + handleInputChange("dataNascimento", e.target.value)} required /> +
+ +
+ + +
+ +
+ + +
+ +
+ + handleInputChange("naturality", e.target.value)} /> +
+ +
+ + +
+ +
+ + handleInputChange("profession", e.target.value)} /> +
+ +
+ + +
+ +
+ + handleInputChange("motherName", e.target.value)} /> +
+ +
+ + handleInputChange("motherProfession", e.target.value)} /> +
+ +
+ + handleInputChange("fatherName", e.target.value)} /> +
+ +
+ + handleInputChange("fatherProfession", e.target.value)} /> +
+ +
+ + handleInputChange("guardianName", e.target.value)} /> +
+ +
+ + handleInputChange("guardianCpf", e.target.value)} placeholder="000.000.000-00" /> +
+ +
+ + handleInputChange("spouseName", e.target.value)} /> +
+
+ +
+
+ setIsGuiaConvenio(checked === true)} /> + +
+
+ +
+ +