From 425f63f8a7b7b909c93fa0141c30d28a0dd28265 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Mon, 3 Nov 2025 19:39:08 -0300 Subject: [PATCH 1/2] 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 66212930e8c5f1fa70ac075eb4793da071a13004 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Tue, 4 Nov 2025 14:26:45 -0300 Subject: [PATCH 2/2] =?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)} /> + +
+
+ +
+ +