diff --git a/app/doctor/consultas/page.tsx b/app/doctor/consultas/page.tsx index b8edfda..0bc765f 100644 --- a/app/doctor/consultas/page.tsx +++ b/app/doctor/consultas/page.tsx @@ -1,272 +1,238 @@ +// ARQUIVO COMPLETO COM A INTERFACE CORRIGIDA: app/doctor/consultas/page.tsx + "use client"; import type React from "react"; -import { useState, useEffect } from "react"; -import DoctorLayout from "@/components/doctor-layout"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useState, useEffect, useMemo } from "react"; +import { useAuthLayout } from "@/hooks/useAuthLayout"; +import { appointmentsService } from "@/services/appointmentsApi.mjs"; +import { patientsService } from "@/services/patientsApi.mjs"; +import { doctorsService } from "@/services/doctorsApi.mjs"; + +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; +import { Separator } from "@/components/ui/separator"; +import { Clock, Calendar as CalendarIcon, User, X, RefreshCw, Loader2, MapPin, Phone, List } from "lucide-react"; +import { format, isFuture, parseISO, isValid, isToday, isTomorrow } from "date-fns"; +import { ptBR } from "date-fns/locale"; import { toast } from "sonner"; +import Sidebar from "@/components/Sidebar"; -// 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 - -const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; - -// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE --- -interface LocalStorageAppointment { - id: number; - patientName: string; - doctor: string; - specialty: string; - date: string; // Data no formato YYYY-MM-DD - time: string; // Hora no formato HH:MM - status: "agendada" | "confirmada" | "cancelada" | "realizada"; - location: string; - phone: string; +// Interfaces (sem alteração) +interface EnrichedAppointment { + id: string; + patientName: string; + patientPhone: string; + scheduled_at: string; + status: "requested" | "confirmed" | "completed" | "cancelled" | "checked_in" | "no_show"; + location: 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(); -}; - -// --- COMPONENTE PRINCIPAL --- - export default function DoctorAppointmentsPage() { - const [allAppointments, setAllAppointments] = useState([]); - const [filteredAppointments, setFilteredAppointments] = useState([]); - const [isLoading, setIsLoading] = useState(true); + const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole: "medico" }); + + const [allAppointments, setAllAppointments] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [selectedDate, setSelectedDate] = useState(new Date()); - // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) - const [bookedDays, setBookedDays] = useState([]); + const fetchAppointments = async (authUserId: string) => { + setIsLoading(true); + try { + const allDoctors = await doctorsService.list(); + const currentDoctor = allDoctors.find((doc: any) => doc.user_id === authUserId); + if (!currentDoctor) { + toast.error("Perfil de médico não encontrado para este usuário."); + return setIsLoading(false); + } + const doctorId = currentDoctor.id; - // NOVO ESTADO 2: Armazena a data selecionada no calendário - const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date()); + const [appointmentsList, patientsList] = await Promise.all([ + appointmentsService.search_appointment(`doctor_id=eq.${doctorId}&order=scheduled_at.asc`), + patientsService.list() + ]); - useEffect(() => { - loadAppointments(); - }, []); + const patientsMap = new Map( + patientsList.map((p: any) => [p.id, { name: p.full_name, phone: p.phone_mobile }]) + ); - // Efeito para filtrar a lista sempre que o calendário ou a lista completa for atualizada - useEffect(() => { - if (selectedCalendarDate) { - const dateString = format(selectedCalendarDate, 'yyyy-MM-dd'); + const enrichedAppointments = appointmentsList.map((apt: any) => ({ + id: apt.id, + patientName: patientsMap.get(apt.patient_id)?.name || "Paciente Desconhecido", + patientPhone: patientsMap.get(apt.patient_id)?.phone || "N/A", + scheduled_at: apt.scheduled_at, + status: apt.status, + location: "Consultório Principal", + })); - // Filtra a lista completa de agendamentos pela data selecionada - const todayAppointments = allAppointments - .filter(app => app.date === dateString) - .sort((a, b) => a.time.localeCompare(b.time)); // Ordena por hora + setAllAppointments(enrichedAppointments); + } catch (error) { + console.error("Erro ao carregar a agenda:", error); + toast.error("Não foi possível carregar sua agenda."); + } finally { + setIsLoading(false); + } + }; - setFilteredAppointments(todayAppointments); - } else { - // Se nenhuma data estiver selecionada (ou se for limpa), mostra todos (ou os de hoje) - const todayDateString = format(new Date(), 'yyyy-MM-dd'); - const todayAppointments = allAppointments - .filter(app => app.date === todayDateString) - .sort((a, b) => a.time.localeCompare(b.time)); + useEffect(() => { + if (user?.id) { + fetchAppointments(user.id); + } + }, [user]); - setFilteredAppointments(todayAppointments); - } - }, [allAppointments, selectedCalendarDate]); + const groupedAppointments = useMemo(() => { + const appointmentsToDisplay = selectedDate + ? allAppointments.filter(app => app.scheduled_at && app.scheduled_at.startsWith(format(selectedDate, "yyyy-MM-dd"))) + : allAppointments.filter(app => { + if (!app.scheduled_at) return false; + const dateObj = parseISO(app.scheduled_at); + return isValid(dateObj) && isFuture(dateObj); + }); - const loadAppointments = () => { - setIsLoading(true); - try { - const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY); - const allAppts: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : []; + return appointmentsToDisplay.reduce((acc, appointment) => { + const dateKey = format(parseISO(appointment.scheduled_at), "yyyy-MM-dd"); + if (!acc[dateKey]) acc[dateKey] = []; + acc[dateKey].push(appointment); + return acc; + }, {} as Record); + }, [allAppointments, selectedDate]); - // ***** NENHUM FILTRO POR MÉDICO AQUI (Como solicitado) ***** - const appointmentsToShow = allAppts; + const bookedDays = useMemo(() => { + return allAppointments + .map(app => app.scheduled_at ? new Date(app.scheduled_at) : null) + .filter((date): date is Date => date !== null); + }, [allAppointments]); - // 1. EXTRAI E PREPARA AS DATAS PARA O CALENDÁRIO - const uniqueBookedDates = Array.from(new Set(appointmentsToShow.map(app => app.date))); + const formatDisplayDate = (dateString: string) => { + const date = parseISO(dateString); + if (isToday(date)) return `Hoje, ${format(date, "dd 'de' MMMM", { locale: ptBR })}`; + if (isTomorrow(date)) return `Amanhã, ${format(date, "dd 'de' MMMM", { locale: ptBR })}`; + return format(date, "EEEE, dd 'de' MMMM", { locale: ptBR }); + }; - // Converte YYYY-MM-DD para objetos Date, garantindo que o tempo seja meia-noite (00:00:00) - const dateObjects = uniqueBookedDates.map(dateString => new Date(dateString + 'T00:00:00')); + const statusPT: Record = { + confirmed: "Confirmada", + completed: "Concluída", + cancelled: "Cancelada", + requested: "Solicitada", + no_show: "oculta", + checked_in: "Aguardando", + }; - setAllAppointments(appointmentsToShow); - setBookedDays(dateObjects); - toast.success("Agenda atualizada com sucesso!"); - } catch (error) { - console.error("Erro ao carregar a agenda do LocalStorage:", error); - toast.error("Não foi possível carregar sua agenda."); - } finally { - setIsLoading(false); - } - }; + const getStatusVariant = (status: EnrichedAppointment['status']) => { + switch (status) { + case "confirmed": case "checked_in": return "text-foreground bg-blue-100 hover:bg-blue-150"; + case "completed": return "text-foreground bg-green-100 hover:bg-green-150"; + case "cancelled": case "no_show": return "text-foreground bg-red-200 hover:bg-red-250"; + case "requested": return "text-foreground bg-yellow-100 hover:bg-yellow-150"; + default: return "border-gray bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90"; + } + }; - const getStatusVariant = (status: LocalStorageAppointment['status']) => { - // ... (código mantido) - switch (status) { - case "confirmada": - case "agendada": - return "default"; - case "realizada": - return "secondary"; - case "cancelada": - return "destructive"; - default: - return "outline"; - } - }; + const handleCancel = async (id: string) => { + // ... (função sem alteração) + }; + const handleReSchedule = (id: string) => { + // ... (função sem alteração) + }; - const handleCancel = (id: number) => { - // ... (código mantido para cancelamento) - const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY); - const allAppts: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : []; + if (isAuthLoading) { + return
Carregando...
; + } - const updatedAppointments = allAppts.map(app => - app.id === id ? { ...app, status: "cancelada" as const } : app - ); + return ( + +
+
+

Agenda Médica

+

Consultas para {user?.name || "você"}

+
+
+

+ {selectedDate ? `Agenda de ${format(selectedDate, "dd/MM/yyyy")}` : "Próximas Consultas"} +

+
+ + +
+
+
+
+ + Filtrar por DataSelecione um dia para ver os detalhes. + + + + +
+
+ {isLoading ? ( +
+ ) : Object.keys(groupedAppointments).length === 0 ? ( + + Nenhuma consulta encontrada +

{selectedDate ? "Não há agendamentos para esta data." : "Não há próximas consultas agendadas."}

+
+ ) : ( + Object.entries(groupedAppointments).map(([date, appointmentsForDay]) => ( +
+

{formatDisplayDate(date)}

+
+ {appointmentsForDay.map((appointment) => { + const showActions = appointment.status === "requested" || appointment.status === "confirmed"; + const scheduledAtDate = parseISO(appointment.scheduled_at); + return ( + // *** INÍCIO DA MUDANÇA NO CARD *** + + + {/* Coluna 1: Nome e Hora */} +
+
+ + {appointment.patientName} +
+
+ + {format(scheduledAtDate, "HH:mm")} +
+
- localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments)); - loadAppointments(); - toast.info(`Consulta cancelada com sucesso.`); - }; + {/* Coluna 2: Status e Telefone */} +
+ {statusPT[appointment.status].replace('_', ' ')} +
+ + {appointment.patientPhone} +
+
- const handleReSchedule = (id: number) => { - toast.info(`Reagendamento da Consulta ID: ${id}. Navegar para a página de agendamento.`); - }; - - const displayDate = selectedCalendarDate ? - new Date(selectedCalendarDate).toLocaleDateString("pt-BR", { weekday: 'long', day: '2-digit', month: 'long' }) : - "Selecione uma data"; - - - return ( - -
-
-

Agenda Médica Centralizada

-

Todas as consultas do sistema são exibidas aqui ({LOGGED_IN_DOCTOR_NAME})

-
- -
-

Consultas para: {displayDate}

- -
- - {/* NOVO LAYOUT DE DUAS COLUNAS */} -
- - {/* COLUNA 1: CALENDÁRIO */} -
- - - - - Calendário - -

Dias em azul possuem agendamentos.

-
- - - + {/* Coluna 3: Ações */} +
+ {showActions && ( +
+ + +
+ )} +
+
-
- - {/* COLUNA 2: LISTA DE CONSULTAS FILTRADAS */} -
- {isLoading ? ( -

Carregando a agenda...

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

Nenhuma consulta encontrada para a data selecionada.

- ) : ( - filteredAppointments.map((appointment) => { - const showActions = appointment.status === "agendada" || appointment.status === "confirmada"; - - return ( - - - - - {appointment.patientName} - - - {appointment.status} - - - - - {/* Detalhes e Ações... (mantidos) */} -
-
- - Médico: {appointment.doctor} -
-
- - {new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: "UTC" })} -
-
- - {appointment.time} -
-
- -
-
- - {appointment.location} -
-
- - {appointment.phone || "N/A"} -
-
- -
- {showActions && ( -
- - -
- )} -
-
-
- ); - }) - )} -
+ // *** FIM DA MUDANÇA NO CARD *** + ); + })} +
+
-
- - ); + )) + )} +
+
+
+ + ); } \ No newline at end of file diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index cf9bad5..45fd3d7 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -1,37 +1,62 @@ "use client"; -import DoctorLayout from "@/components/doctor-layout"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Calendar, Clock, User, Trash2 } from "lucide-react"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import Link from "next/link"; import { useEffect, useState } from "react"; import { toast } from "@/hooks/use-toast"; +// --- IMPORTS ADICIONADOS PARA A CORREÇÃO --- +import { useAuthLayout } from "@/hooks/useAuthLayout"; +import { patientsService } from "@/services/patientsApi.mjs"; +// --- FIM DOS IMPORTS ADICIONADOS --- + +import { appointmentsService } from "@/services/appointmentsApi.mjs"; +import { format, parseISO, isAfter, isSameMonth, startOfToday } from "date-fns"; +import { ptBR } from "date-fns/locale"; + import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { exceptionsService } from "@/services/exceptionApi.mjs"; import { doctorsService } from "@/services/doctorsApi.mjs"; import { usersService } from "@/services/usersApi.mjs"; +import Sidebar from "@/components/Sidebar"; +import WeeklyScheduleCard from "@/components/ui/WeeklyScheduleCard"; type Availability = { - id: string; - doctor_id: string; - weekday: string; - start_time: string; - end_time: string; - slot_minutes: number; - appointment_type: string; - active: boolean; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string | null; + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; + slot_minutes: number; + appointment_type: string; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; }; type Schedule = { - weekday: object; + weekday: object; }; type Doctor = { @@ -61,36 +86,36 @@ type Doctor = { updated_by: string | null; max_days_in_advance: number; rating: number | null; -} +}; interface UserPermissions { - isAdmin: boolean; - isManager: boolean; - isDoctor: boolean; - isSecretary: boolean; - isAdminOrManager: boolean; + isAdmin: boolean; + isManager: boolean; + isDoctor: boolean; + isSecretary: boolean; + isAdminOrManager: boolean; } interface UserData { - user: { - id: string; - email: string; - email_confirmed_at: string | null; - created_at: string | null; - last_sign_in_at: string | null; - }; - profile: { - id: string; - full_name: string; - email: string; - phone: string; - avatar_url: string | null; - disabled: boolean; - created_at: string | null; - updated_at: string | null; - }; - roles: string[]; - permissions: UserPermissions; + user: { + id: string; + email: string; + email_confirmed_at: string | null; + created_at: string | null; + last_sign_in_at: string | null; + }; + profile: { + id: string; + full_name: string; + email: string; + phone: string; + avatar_url: string | null; + disabled: boolean; + created_at: string | null; + updated_at: string | null; + }; + roles: string[]; + permissions: UserPermissions; } interface Exception { @@ -98,7 +123,7 @@ interface Exception { doctor_id: string; date: string; // formato YYYY-MM-DD start_time: string | null; // null = dia inteiro - end_time: string | null; // null = dia inteiro + end_time: string | null; // null = dia inteiro kind: "bloqueio" | "disponibilidade"; // tipos conhecidos reason: string | null; // pode ser null created_at: string; // timestamp ISO @@ -106,7 +131,10 @@ interface Exception { } export default function PatientDashboard() { - const [loggedDoctor, setLoggedDoctor] = useState(); + // --- USA O HOOK DE AUTENTICAÇÃO PARA PEGAR O USUÁRIO LOGADO --- + const { user } = useAuthLayout({ requiredRole: ['medico'] }); + + const [loggedDoctor, setLoggedDoctor] = useState(null); const [userData, setUserData] = useState(); const [availability, setAvailability] = useState(null); const [exceptions, setExceptions] = useState([]); @@ -116,56 +144,79 @@ export default function PatientDashboard() { const [exceptionToDelete, setExceptionToDelete] = useState(null); const [error, setError] = useState(null); - // Mapa de tradução - const weekdaysPT: Record = { - sunday: "Domingo", - monday: "Segunda", - tuesday: "Terça", - wednesday: "Quarta", - thursday: "Quinta", - friday: "Sexta", - saturday: "Sábado", - }; + // --- ESTADOS PARA OS CARDS ATUALIZADOS --- + const [nextAppointment, setNextAppointment] = useState(null); + const [monthlyCount, setMonthlyCount] = useState(0); + const weekdaysPT: Record = { sunday: "Domingo", monday: "Segunda", tuesday: "Terça", wednesday: "Quarta", thursday: "Quinta", friday: "Sexta", saturday: "Sábado" }; + + // ▼▼▼ LÓGICA DE BUSCA CORRIGIDA E ATUALIZADA ▼▼▼ useEffect(() => { - const fetchData = async () => { - try { - const doctorsList: Doctor[] = await doctorsService.list(); - const doctor = doctorsList[0]; + const fetchData = async () => { + if (!user?.id) return; // Aguarda o usuário ser carregado - // Salva no estado - setLoggedDoctor(doctor); + try { + // Encontra o perfil de médico correspondente ao usuário logado + const doctorsList: Doctor[] = await doctorsService.list(); + const currentDoctor = doctorsList.find(doc => doc.user_id === user.id); - // 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); + if (!currentDoctor) { + setError("Perfil de médico não encontrado para este usuário."); + return; + } + setLoggedDoctor(currentDoctor); - // 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); + // Busca todos os dados necessários em paralelo + const [appointmentsList, patientsList, availabilityList, exceptionsList] = await Promise.all([ + appointmentsService.list(), + patientsService.list(), + AvailabilityService.list(), + exceptionsService.list() + ]); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; + // Mapeia pacientes por ID para consulta rápida + const patientsMap = new Map(patientsList.map((p: any) => [p.id, p.full_name])); - fetchData(); -}, []); + // Filtra e enriquece as consultas APENAS do médico logado + const doctorAppointments = appointmentsList + .filter((apt: any) => apt.doctor_id === currentDoctor.id) + .map((apt: any): EnrichedAppointment => ({ + ...apt, + patientName: patientsMap.get(apt.patient_id) || "Paciente Desconhecido", + })); + + // 1. Lógica para "Próxima Consulta" + const today = startOfToday(); + const upcomingAppointments = doctorAppointments + .filter(apt => isAfter(parseISO(apt.scheduled_at), today)) + .sort((a, b) => new Date(a.scheduled_at).getTime() - new Date(b.scheduled_at).getTime()); + setNextAppointment(upcomingAppointments[0] || null); + + // 2. Lógica para "Consultas Este Mês" (apenas ativas) + const activeStatuses = ['confirmed', 'requested', 'checked_in']; + const currentMonthAppointments = doctorAppointments.filter(apt => + isSameMonth(parseISO(apt.scheduled_at), new Date()) && activeStatuses.includes(apt.status) + ); + setMonthlyCount(currentMonthAppointments.length); + + // Busca e filtra o restante dos dados + setAvailability(availabilityList.filter((d: any) => d.doctor_id === currentDoctor.id)); + setExceptions(exceptionsList.filter((e: any) => e.doctor_id === currentDoctor.id)); + + } catch (e: any) { + setError(e?.message || "Erro ao buscar dados do dashboard"); + console.error("Erro no dashboard:", e); + } + }; + + fetchData(); + }, [user]); // A busca de dados agora depende do usuário logado + // ▲▲▲ FIM DA LÓGICA DE BUSCA ATUALIZADA ▲▲▲ - // 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); @@ -173,106 +224,98 @@ export default function PatientDashboard() { const handleDeleteException = async (ExceptionId: string) => { try { - alert(ExceptionId) const res = await exceptionsService.delete(ExceptionId); - - let message = "Exceção deletada com sucesso"; - try { - if (res) { - throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); - } else { - console.log(message); - } - } catch {} - - toast({ - title: "Sucesso", - description: message, - }); - + if (res && res.error) { throw new Error(res.message || "A API retornou um erro"); } + toast({ title: "Sucesso", description: "Exceção deletada com sucesso" }); setExceptions((prev: Exception[]) => prev.filter((p) => String(p.id) !== String(ExceptionId))); } catch (e: any) { - toast({ - title: "Erro", - description: e?.message || "Não foi possível deletar a exceção", - }); + toast({ title: "Erro", description: e?.message || "Não foi possível deletar a exceção" }); } setDeleteDialogOpen(false); setExceptionToDelete(null); }; function formatAvailability(data: Availability[]) { - // Agrupar os horários por dia da semana + if (!data) return {}; 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, - }); - + if (!acc[weekday]) acc[weekday] = []; + 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]); + useEffect(() => { + if (availability) { + const formatted = formatAvailability(availability); + setSchedule(formatted); + } + }, [availability]); - return ( - -
-
-

Dashboard

-

Bem-vindo ao seu portal de consultas médicas

-
+ return ( + +
+
+

Dashboard

+

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

+
+ {/* ▼▼▼ CARD "PRÓXIMA CONSULTA" CORRIGIDO PARA MOSTRAR NOME DO PACIENTE ▼▼▼ */} Próxima Consulta -
02 out
-

Dr. Silva - 14:30

+ {nextAppointment ? ( + <> +
+ {format(parseISO(nextAppointment.scheduled_at), "dd MMM", { locale: ptBR })} +
+

+ {nextAppointment.patientName} - {format(parseISO(nextAppointment.scheduled_at), "HH:mm")} +

+ + ) : ( + <> +
Nenhuma
+

Sem próximas consultas

+ + )}
+ {/* ▲▲▲ FIM DO CARD ATUALIZADO ▲▲▲ */} + {/* ▼▼▼ CARD "CONSULTAS ESTE MÊS" CORRIGIDO PARA CONTAGEM CORRETA ▼▼▼ */} Consultas Este Mês -
4
-

4 agendadas

+
{monthlyCount}
+

{monthlyCount === 1 ? '1 agendada' : `${monthlyCount} agendadas`}

+ {/* ▲▲▲ FIM DO CARD ATUALIZADO ▲▲▲ */} - - - Perfil - - - -
100%
-

Dados completos

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

Dados completos

+
+
+
+ {/* O restante do código permanece o mesmo */}
@@ -316,31 +359,7 @@ export default function PatientDashboard() { Horário Semanal Confira rapidamente a sua disponibilidade da semana - - {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { - const times = schedule[day] || []; - return ( -
-
-
-

{weekdaysPT[day]}

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

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

- )) - ) : ( -

Sem horário

- )} -
-
-
- ); - })} -
+ {loggedDoctor && }
@@ -353,7 +372,6 @@ export default function PatientDashboard() { {exceptions && exceptions.length > 0 ? ( exceptions.map((ex: Exception) => { - // Formata data e hora const date = new Date(ex.date).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", @@ -361,18 +379,18 @@ export default function PatientDashboard() { timeZone: "UTC" }); - const startTime = formatTime(ex.start_time); - const endTime = formatTime(ex.end_time); + const startTime = formatTime(ex.start_time); + const endTime = formatTime(ex.end_time); return (

{date}

-

+

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

@@ -409,6 +427,6 @@ export default function PatientDashboard() {
- + ); -} +} \ No newline at end of file diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx index 3e7b316..932cb3f 100644 --- a/app/doctor/disponibilidade/excecoes/page.tsx +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -3,14 +3,12 @@ import type React from "react"; import Link from "next/link"; import { useState, useEffect } from "react"; -import DoctorLayout from "@/components/doctor-layout"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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 { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; +import { Calendar as CalendarIcon, RefreshCw } from "lucide-react"; import { useRouter } from "next/navigation"; import { toast } from "@/hooks/use-toast"; import { exceptionsService } from "@/services/exceptionApi.mjs"; @@ -19,6 +17,7 @@ import { exceptionsService } from "@/services/exceptionApi.mjs"; import { Calendar } from "@/components/ui/calendar"; import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas import { doctorsService } from "@/services/doctorsApi.mjs"; +import Sidebar from "@/components/Sidebar"; type Doctor = { id: string; @@ -147,7 +146,7 @@ export default function ExceptionPage() { const displayDate = selectedCalendarDate ? new Date(selectedCalendarDate).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", month: "long" }) : "Selecione uma data"; return ( - +

Adicione exceções

@@ -254,6 +253,6 @@ export default function ExceptionPage() {
- + ); } diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 36f0e98..7fd25fd 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -6,8 +6,13 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import DoctorLayout from "@/components/doctor-layout"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { AvailabilityService } from "@/services/availabilityApi.mjs"; import { usersService } from "@/services/usersApi.mjs"; @@ -15,162 +20,203 @@ import { doctorsService } from "@/services/doctorsApi.mjs"; import { toast } from "@/hooks/use-toast"; import { useRouter } from "next/navigation"; -import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, +} from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Edit, Trash2 } from "lucide-react"; import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal"; -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import Sidebar from "@/components/Sidebar"; // ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas) interface UserPermissions { - isAdmin: boolean; - isManager: boolean; - isDoctor: boolean; - isSecretary: boolean; - isAdminOrManager: boolean; + isAdmin: boolean; + isManager: boolean; + isDoctor: boolean; + isSecretary: boolean; + isAdminOrManager: boolean; } interface UserData { - user: { - id: string; - email: string; - email_confirmed_at: string | null; - created_at: string | null; - last_sign_in_at: string | null; - }; - profile: { - id: string; - full_name: string; - email: string; - phone: string; - avatar_url: string | null; - disabled: boolean; - created_at: string | null; - updated_at: string | null; - }; - roles: string[]; - permissions: UserPermissions; + user: { + id: string; + email: string; + email_confirmed_at: string | null; + created_at: string | null; + last_sign_in_at: string | null; + }; + profile: { + id: string; + full_name: string; + email: string; + phone: string; + avatar_url: string | null; + disabled: boolean; + created_at: string | null; + updated_at: string | null; + }; + roles: string[]; + permissions: UserPermissions; } type Doctor = { - id: string; - user_id: string | null; - crm: string; - crm_uf: string; - specialty: string; - full_name: string; - cpf: string; - email: string; - phone_mobile: string | null; - phone2: string | null; - cep: string | null; - street: string | null; - number: string | null; - complement: string | null; - neighborhood: string | null; - city: string | null; - state: string | null; - birth_date: string | null; - rg: string | null; - active: boolean; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string | null; - max_days_in_advance: number; - rating: number | null; -} + id: string; + user_id: string | null; + crm: string; + crm_uf: string; + specialty: string; + full_name: string; + cpf: string; + email: string; + phone_mobile: string | null; + phone2: string | null; + cep: string | null; + street: string | null; + number: string | null; + complement: string | null; + neighborhood: string | null; + city: string | null; + state: string | null; + birth_date: string | null; + rg: string | null; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; + max_days_in_advance: number; + rating: number | null; +}; type Availability = { - id: string; - doctor_id: string; - weekday: string; - start_time: string; - end_time: string; - slot_minutes: number; - appointment_type: string; - active: boolean; - created_at: string; - updated_at: string; - created_by: string; - updated_by: string | null; + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; + slot_minutes: number; + appointment_type: string; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; }; export default function AvailabilityPage() { - const [error, setError] = useState(null); - const router = useRouter(); - const [isLoading, setIsLoading] = useState(false); - const [schedule, setSchedule] = useState>({}); - const formatTime = (time?: string | null) => time?.split(":")?.slice(0, 2).join(":") ?? ""; - const [userData, setUserData] = useState(); - const [availability, setAvailability] = useState(null); - const [doctorId, setDoctorId] = useState(); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [modalidadeConsulta, setModalidadeConsulta] = useState(""); - const [selectedAvailability, setSelectedAvailability] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - const selectAvailability = (schedule: { start: string; end: string;}, day: string) => { - const selected = availability.filter((a: Availability) => - a.start_time === schedule.start && - a.end_time === schedule.end && - a.weekday === day - ); - setSelectedAvailability(selected[0]); - } + const [error, setError] = useState(null); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [schedule, setSchedule] = useState< + Record + >({}); + const formatTime = (time?: string | null) => + time?.split(":")?.slice(0, 2).join(":") ?? ""; + const [userData, setUserData] = useState(); + const [availability, setAvailability] = useState(null); + const [doctorId, setDoctorId] = useState(); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [modalidadeConsulta, setModalidadeConsulta] = useState(""); + const [selectedAvailability, setSelectedAvailability] = + useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); - const handleOpenModal = (schedule: { start: string; end: string;}, day: string) => { - selectAvailability(schedule, day) - setIsModalOpen(true); - }; - - const handleCloseModal = () => { - setSelectedAvailability(null); - setIsModalOpen(false); + const selectAvailability = ( + schedule: { start: string; end: string }, + day: string + ) => { + const selected = availability.filter( + (a: Availability) => + a.start_time === schedule.start && + a.end_time === schedule.end && + a.weekday === day + ); + setSelectedAvailability(selected[0]); + }; + + const handleOpenModal = ( + schedule: { start: string; end: string }, + day: string + ) => { + selectAvailability(schedule, day); + setIsModalOpen(true); + }; + + const handleCloseModal = () => { + setSelectedAvailability(null); + setIsModalOpen(false); + }; + + const handleEdit = async (formData: { + start_time: ""; + end_time: ""; + slot_minutes: ""; + appointment_type: ""; + id: ""; + }) => { + if (isLoading) return; + setIsLoading(true); + + const apiPayload = { + start_time: formData.start_time, + end_time: formData.end_time, + slot_minutes: formData.slot_minutes, + appointment_type: formData.appointment_type, }; + console.log(apiPayload); - const handleEdit = async (formData:{ start_time: "", end_time: "", slot_minutes: "", appointment_type: "", id:""}) => { - if (isLoading) return; - setIsLoading(true); + try { + const res = await AvailabilityService.update(formData.id, apiPayload); + console.log(res); - const apiPayload = { - start_time: formData.start_time, - end_time: formData.end_time, - slot_minutes: formData.slot_minutes, - appointment_type: formData.appointment_type, - }; - console.log(apiPayload); - - try { - const res = await AvailabilityService.update(formData.id, apiPayload); - console.log(res); - - let message = "disponibilidade editada com sucesso"; - try { - if (!res[0].id) { - throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); - } else { - console.log(message); - } - } catch {} - - toast({ - title: "Sucesso", - description: message, - }); - router.push("#") - } catch (err: any) { - toast({ - title: "Erro", - description: err?.message || "Não foi possível editar a disponibilidade", - }); - } finally { - setIsLoading(false); - handleCloseModal(); - fetchData() + let message = "disponibilidade editada com sucesso"; + try { + if (!res[0].id) { + throw new Error( + `${res.error} ${res.message}` || "A API retornou erro" + ); + } else { + console.log(message); } - }; + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + router.push("#"); + } catch (err: any) { + toast({ + title: "Erro", + description: + err?.message || "Não foi possível editar a disponibilidade", + }); + } finally { + setIsLoading(false); + handleCloseModal(); + fetchData(); + } + }; // Mapa de tradução const weekdaysPT: Record = { @@ -183,95 +229,96 @@ export default function AvailabilityPage() { saturday: "Sábado", }; const fetchData = async () => { - try { - const loggedUser = await usersService.getMe(); - const doctorList = await doctorsService.list(); - setUserData(loggedUser); - const doctor = findDoctorById(loggedUser.user.id, doctorList); - setDoctorId(doctor?.id); - console.log(doctor); - // Busca disponibilidade - const availabilityList = await AvailabilityService.list(); - - // Filtra já com a variável local - const filteredAvail = availabilityList.filter( - (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id - ); - setAvailability(filteredAvail); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; - - useEffect(() => { - fetchData(); - }, []); - - // Função auxiliar para filtrar o id do doctor correspondente ao user logado - function findDoctorById(id: string, doctors: Doctor[]) { - return doctors.find((doctor) => doctor.user_id === id); - } - - - function formatAvailability(data: Availability[]) { - // Agrupar os horários por dia da semana - const schedule = data.reduce((acc: any, item) => { - const { weekday, start_time, end_time } = item; - - // Se o dia ainda não existe, cria o array - if (!acc[weekday]) { - acc[weekday] = []; - } - - // Adiciona o horário do dia - acc[weekday].push({ - start: start_time, - end: end_time, - }); - - return acc; - }, {} as Record); - - return schedule; - } - - useEffect(() => { - if (availability) { - const formatted = formatAvailability(availability); - setSchedule(formatted); - } - }, [availability]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (isLoading) return; - setIsLoading(true); - const form = e.currentTarget; - const formData = new FormData(form); - - const apiPayload = { - doctor_id: doctorId, - weekday: (formData.get("weekday") as string) || undefined, - start_time: (formData.get("horarioEntrada") as string) || undefined, - end_time: (formData.get("horarioSaida") as string) || undefined, - slot_minutes: Number(formData.get("duracaoConsulta")) || undefined, - appointment_type: modalidadeConsulta || undefined, - active: true, - }; - console.log(apiPayload); - try { - const res = await AvailabilityService.create(apiPayload); - console.log(res); + const loggedUser = await usersService.getMe(); + const doctorList = await doctorsService.list(); + setUserData(loggedUser); + const doctor = findDoctorById(loggedUser.user.id, doctorList); + setDoctorId(doctor?.id); + console.log(doctor); + // Busca disponibilidade + const availabilityList = await AvailabilityService.list(); + + // Filtra já com a variável local + const filteredAvail = availabilityList.filter( + (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id + ); + setAvailability(filteredAvail); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; - let message = "disponibilidade cadastrada com sucesso"; - try { - if (!res[0].id) { - throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); - } else { - console.log(message); - } - } catch {} + useEffect(() => { + fetchData(); + }, []); + + // Função auxiliar para filtrar o id do doctor correspondente ao user logado + function findDoctorById(id: string, doctors: Doctor[]) { + return doctors.find((doctor) => doctor.user_id === id); + } + + function formatAvailability(data: Availability[]) { + // Agrupar os horários por dia da semana + const schedule = data.reduce((acc: any, item) => { + const { weekday, start_time, end_time } = item; + + // Se o dia ainda não existe, cria o array + if (!acc[weekday]) { + acc[weekday] = []; + } + + // Adiciona o horário do dia + acc[weekday].push({ + start: start_time, + end: end_time, + }); + + return acc; + }, {} as Record); + + return schedule; + } + + useEffect(() => { + if (availability) { + const formatted = formatAvailability(availability); + setSchedule(formatted); + } + }, [availability]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (isLoading) return; + setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + + const apiPayload = { + doctor_id: doctorId, + weekday: (formData.get("weekday") as string) || undefined, + start_time: (formData.get("horarioEntrada") as string) || undefined, + end_time: (formData.get("horarioSaida") as string) || undefined, + slot_minutes: Number(formData.get("duracaoConsulta")) || undefined, + appointment_type: modalidadeConsulta || undefined, + active: true, + }; + console.log(apiPayload); + + try { + const res = await AvailabilityService.create(apiPayload); + console.log(res); + + let message = "disponibilidade cadastrada com sucesso"; + try { + if (!res[0].id) { + throw new Error( + `${res.error} ${res.message}` || "A API retornou erro" + ); + } else { + console.log(message); + } + } catch {} toast({ title: "Sucesso", @@ -284,14 +331,18 @@ export default function AvailabilityPage() { description: err?.message || "Não foi possível criar a disponibilidade", }); } finally { + fetchData() setIsLoading(false); } }; - const openDeleteDialog = (schedule: { start: string; end: string;}, day: string) => { - selectAvailability(schedule, day) - setDeleteDialogOpen(true); - }; + const openDeleteDialog = ( + schedule: { start: string; end: string }, + day: string + ) => { + selectAvailability(schedule, day); + setDeleteDialogOpen(true); + }; const handleDeleteAvailability = async (AvailabilityId: string) => { try { @@ -318,109 +369,185 @@ export default function AvailabilityPage() { description: e?.message || "Não foi possível deletar a disponibilidade", }); } + fetchData() setDeleteDialogOpen(false); setSelectedAvailability(null); }; - return ( - + return ( + +
+
+
+

+ Definir Disponibilidade +

+

+ Defina sua disponibilidade para consultas{" "} +

+
+
+ +
+
+

Dados

+
-
-
-

Definir Disponibilidade

-

Defina sua disponibilidade para consultas

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

Dados

+ {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */} + {/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */} +
+
+ + +
+
+ + +
+
+ + +
+ {/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */} +
-
- {/* **AJUSTE DE RESPONSIVIDADE: DIAS DA SEMANA** */} -
- - {/* O antigo 'flex gap-4 mt-2 flex-nowrap' foi substituído por um grid responsivo: */} -
- - - - - - - -
-
- - {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */} - {/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */} -
-
- - -
-
- - -
-
- - -
- {/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */} -
- -
- - -
-
-
+
+ + +
+
+
{/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} + {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */}
-
+
{/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */} @@ -452,7 +579,7 @@ export default function AvailabilityPage() {
-

+

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

@@ -506,6 +633,6 @@ export default function AvailabilityPage() { onSubmit={handleEdit} /> - + ); -} \ No newline at end of file +} diff --git a/app/doctor/medicos/[id]/editar/page.tsx b/app/doctor/medicos/[id]/editar/page.tsx index 0049db2..7bc5595 100644 --- a/app/doctor/medicos/[id]/editar/page.tsx +++ b/app/doctor/medicos/[id]/editar/page.tsx @@ -12,7 +12,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { ArrowLeft, Save } from "lucide-react"; import Link from "next/link"; -import DoctorLayout from "@/components/doctor-layout"; +import Sidebar from "@/components/Sidebar"; // Mock data - in a real app, this would come from an API const mockDoctors = [ @@ -124,7 +124,7 @@ export default function EditarMedicoPage() { }; return ( - +
@@ -512,6 +512,6 @@ export default function EditarMedicoPage() {
-
+ ); } diff --git a/app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx b/app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx index 7257b53..acce902 100644 --- a/app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx +++ b/app/doctor/medicos/[id]/laudos/[laudoId]/editar/page.tsx @@ -2,7 +2,6 @@ import { useParams, useRouter } from "next/navigation"; import { useState, useEffect } from "react"; -import DoctorLayout from "@/components/doctor-layout"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -17,6 +16,7 @@ import { format } from "date-fns"; import TiptapEditor from "@/components/ui/tiptap-editor"; import { Skeleton } from "@/components/ui/skeleton"; import { reportsApi } from "@/services/reportsApi.mjs"; +import Sidebar from "@/components/Sidebar"; export default function EditarLaudoPage() { const router = useRouter(); @@ -108,7 +108,7 @@ export default function EditarLaudoPage() { if (loading) { return ( - +
@@ -130,12 +130,12 @@ export default function EditarLaudoPage() {
-
+ ) } return ( - +
@@ -228,6 +228,6 @@ export default function EditarLaudoPage() {
-
+ ); } \ No newline at end of file diff --git a/app/doctor/medicos/[id]/laudos/novo/page.tsx b/app/doctor/medicos/[id]/laudos/novo/page.tsx index 57371ba..1bac56b 100644 --- a/app/doctor/medicos/[id]/laudos/novo/page.tsx +++ b/app/doctor/medicos/[id]/laudos/novo/page.tsx @@ -16,7 +16,7 @@ import { format } from "date-fns"; import TiptapEditor from "@/components/ui/tiptap-editor"; import { reportsApi } from "@/services/reportsApi.mjs"; -import DoctorLayout from "@/components/doctor-layout"; +import Sidebar from "@/components/Sidebar"; @@ -96,7 +96,7 @@ export default function NovoLaudoPage() { }; return ( - +
@@ -184,6 +184,6 @@ export default function NovoLaudoPage() {
-
+ ); } \ No newline at end of file diff --git a/app/doctor/medicos/[id]/laudos/page.tsx b/app/doctor/medicos/[id]/laudos/page.tsx index 981b250..848bb40 100644 --- a/app/doctor/medicos/[id]/laudos/page.tsx +++ b/app/doctor/medicos/[id]/laudos/page.tsx @@ -8,7 +8,7 @@ import Link from 'next/link'; import { useParams } from 'next/navigation'; import { api } from '@/services/api.mjs'; import { reportsApi } from '@/services/reportsApi.mjs'; -import DoctorLayout from '@/components/doctor-layout'; +import Sidebar from '@/components/Sidebar'; export default function LaudosPage() { const [patient, setPatient] = useState(null); @@ -49,7 +49,7 @@ export default function LaudosPage() { const paginate = (pageNumber) => setCurrentPage(pageNumber); return ( - +
{loading ? (

Carregando...

@@ -123,6 +123,6 @@ export default function LaudosPage() { )}
-
+ ); } \ No newline at end of file diff --git a/app/doctor/medicos/novo/page.tsx b/app/doctor/medicos/novo/page.tsx index dee0314..8aa1b75 100644 --- a/app/doctor/medicos/novo/page.tsx +++ b/app/doctor/medicos/novo/page.tsx @@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Upload, Plus, X, ChevronDown } from "lucide-react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import DoctorLayout from "@/components/doctor-layout"; +import Sidebar from "@/components/Sidebar"; export default function NovoMedicoPage() { const [anexosOpen, setAnexosOpen] = useState(false); @@ -24,7 +24,7 @@ export default function NovoMedicoPage() { }; return ( - +
@@ -466,6 +466,6 @@ export default function NovoMedicoPage() {
- + ); } diff --git a/app/doctor/medicos/page.tsx b/app/doctor/medicos/page.tsx index ae26555..7973a65 100644 --- a/app/doctor/medicos/page.tsx +++ b/app/doctor/medicos/page.tsx @@ -1,8 +1,6 @@ -// app/doctor/pacientes/page.tsx (assumindo a localização) "use client"; import { useEffect, useState, useCallback } from "react"; -import DoctorLayout from "@/components/doctor-layout"; import Link from "next/link"; import { DropdownMenu, @@ -21,6 +19,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; +import Sidebar from "@/components/Sidebar"; interface Paciente { id: string; @@ -51,7 +50,7 @@ export default function PacientesPage() { const [isModalOpen, setIsModalOpen] = useState(false); // --- Lógica de Paginação INÍCIO --- - const [itemsPerPage, setItemsPerPage] = useState(5); + const [itemsPerPage, setItemsPerPage] = useState(10); const [currentPage, setCurrentPage] = useState(1); const totalPages = Math.ceil(pacientes.length / itemsPerPage); @@ -70,7 +69,7 @@ export default function PacientesPage() { const goToNextPage = () => { setCurrentPage((prev) => Math.min(totalPages, prev + 1)); }; - + // Lógica para gerar os números das páginas visíveis (máximo de 5) const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { const pages: number[] = []; @@ -87,13 +86,13 @@ export default function PacientesPage() { endPage = Math.min(totalPages, maxVisiblePages); } } - + for (let i = startPage; i <= endPage; i++) { pages.push(i); } return pages; }; - + const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); // Lógica para mudar itens por página, resetando para a página 1 @@ -103,7 +102,6 @@ export default function PacientesPage() { }; // --- Lógica de Paginação FIM --- - const handleOpenModal = (patient: Paciente) => { setSelectedPatient(patient); setIsModalOpen(true); @@ -171,23 +169,26 @@ export default function PacientesPage() { }, [fetchPacientes]); return ( - +
{/* Cabeçalho */} -
+
+ {" "} + {/* Ajustado para flex-col em telas pequenas */}

Pacientes

Lista de pacientes vinculados

- {/* Adicione um seletor de itens por página ao lado de um botão de 'Novo Paciente' se aplicável */} -
+ {/* Controles de filtro e novo paciente */} + {/* Alterado para que o Select e o Link ocupem a largura total em telas pequenas e fiquem lado a lado em telas maiores */} +
- - -
-
-
+ {/* Tabela para Telas Médias e Grandes */} +
+ {" "} + {/* Esconde em telas pequenas */} - - + - + {loading ? ( - ) : error ? ( - + ) : pacientes.length === 0 ? ( - @@ -254,7 +265,7 @@ export default function PacientesPage() { className="border-b border-border hover:bg-accent/40 transition-colors" > -
Nome + + Nome + Telefone @@ -226,24 +226,35 @@ export default function PacientesPage() { Próximo atendimento Ações + Ações +
+ Carregando pacientes...
{`Erro: ${error}`}{`Erro: ${error}`}
+ Nenhum paciente encontrado
{p.nome} + {p.telefone} @@ -278,7 +289,9 @@ export default function PacientesPage() { - handleOpenModal(p)}> + handleOpenModal(p)} + > Ver detalhes @@ -290,11 +303,11 @@ export default function PacientesPage() { { - // Simulação de exclusão (A exclusão real deve ser feita via API) - const newPacientes = pacientes.filter((pac) => pac.id !== p.id); + const newPacientes = pacientes.filter( + (pac) => pac.id !== p.id + ); setPacientes(newPacientes); alert(`Paciente ID: ${p.id} excluído`); - // Necessário chamar a API de exclusão aqui }} className="text-red-600 focus:bg-red-50 focus:text-red-600" > @@ -311,10 +324,90 @@ export default function PacientesPage() {
- {/* Paginação ATUALIZADA */} + {/* Layout em Cards/Lista para Telas Pequenas */} +
+ {" "} + {/* Visível apenas em telas pequenas */} + {loading ? ( +
+ + Carregando pacientes... +
+ ) : error ? ( +
{`Erro: ${error}`}
+ ) : pacientes.length === 0 ? ( +
+ Nenhum paciente encontrado +
+ ) : ( + currentItems.map((p) => ( +
+
+ {" "} + {/* Adicionado padding à direita */} +
+ {" "} + {/* Aumentado a fonte e break-words para evitar corte do nome */} + {p.nome || "—"} +
+ {/* Removido o 'truncate' e adicionado 'break-words' no telefone */} +
+ Telefone: **{p.telefone || "N/A"}** +
+
+
+ + + + + + handleOpenModal(p)}> + + Ver detalhes + + + + + Laudos + + + + alert(`Agenda para paciente ID: ${p.id}`) + } + > + + Ver agenda + + { + const newPacientes = pacientes.filter( + (pac) => pac.id !== p.id + ); + setPacientes(newPacientes); + alert(`Paciente ID: ${p.id} excluído`); + }} + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > + + Excluir + + + +
+
+ )) + )} +
+ + {/* Paginação */} {totalPages > 1 && (
- {/* Botão Anterior */} ))} - + {/* Botão Próximo */} -
)} - {/* Fim da Paginação ATUALIZADA */} -
@@ -360,6 +450,6 @@ export default function PacientesPage() { isOpen={isModalOpen} onClose={handleCloseModal} /> - + ); -} \ No newline at end of file +} diff --git a/app/finance/home/page.tsx b/app/finance/home/page.tsx index c9e0567..1c07932 100644 --- a/app/finance/home/page.tsx +++ b/app/finance/home/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import FinancierLayout from "@/components/finance-layout"; +import Sidebar from "@/components/Sidebar"; interface Paciente { id: string; @@ -14,43 +14,10 @@ interface Paciente { } export default function PacientesPage() { - const [pacientes, setPacientes] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - async function fetchPacientes() { - try { - setLoading(true); - setError(null); - const res = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes"); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const json = await res.json(); - const items = Array.isArray(json?.data) ? json.data : []; - - const mapped = items.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.nome ?? "", - telefone: p?.contato?.celular ?? p?.contato?.telefone1 ?? p?.telefone ?? "", - cidade: p?.endereco?.cidade ?? p?.cidade ?? "", - estado: p?.endereco?.estado ?? p?.estado ?? "", - ultimoAtendimento: p.ultimo_atendimento ?? p.ultimoAtendimento ?? "", - proximoAtendimento: p.proximo_atendimento ?? p.proximoAtendimento ?? "", - })); - - setPacientes(mapped); - } catch (e: any) { - setError(e?.message || "Erro ao carregar pacientes"); - } finally { - setLoading(false); - } - } - fetchPacientes(); - }, []); return ( - +
-
+ ); } diff --git a/app/globals.css b/app/globals.css index d0b1400..95ff1b4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,8 +1,6 @@ -@import 'tailwindcss'; -@import 'tw-animate-css'; - +@import "tailwindcss"; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); - :root { --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); @@ -75,33 +73,33 @@ } .high-contrast { - --background: oklch(0 0 0); - --foreground: oklch(1 0.5 100); - --card: oklch(0 0 0); - --card-foreground: oklch(1 0.5 100); - --popover: oklch(0 0 0); - --popover-foreground: oklch(1 0.5 100); - --primary: oklch(1 0.5 100); - --primary-foreground: oklch(0 0 0); - --secondary: oklch(0 0 0); - --secondary-foreground: oklch(1 0.5 100); - --muted: oklch(0 0 0); - --muted-foreground: oklch(1 0.5 100); - --accent: oklch(0 0 0); - --accent-foreground: oklch(1 0.5 100); - --destructive: oklch(0.5 0.3 30); - --destructive-foreground: oklch(0 0 0); - --border: oklch(1 0.5 100); - --input: oklch(0 0 0); - --ring: oklch(1 0.5 100); - --sidebar: oklch(0 0 0); - --sidebar-foreground: oklch(1 0.5 100); - --sidebar-primary: oklch(1 0.5 100); - --sidebar-primary-foreground: oklch(0 0 0); - --sidebar-accent: oklch(0 0 0); - --sidebar-accent-foreground: oklch(1 0.5 100); - --sidebar-border: oklch(1 0.5 100); - --sidebar-ring: oklch(1 0.5 100); + --background: oklch(0 0 0); + --foreground: oklch(1 0.5 100); + --card: oklch(0 0 0); + --card-foreground: oklch(1 0.5 100); + --popover: oklch(0 0 0); + --popover-foreground: oklch(1 0.5 100); + --primary: oklch(1 0.5 100); + --primary-foreground: oklch(0 0 0); + --secondary: oklch(0 0 0); + --secondary-foreground: oklch(1 0.5 100); + --muted: oklch(0 0 0); + --muted-foreground: oklch(1 0.5 100); + --accent: oklch(0 0 0); + --accent-foreground: oklch(1 0.5 100); + --destructive: oklch(0.5 0.3 30); + --destructive-foreground: oklch(0 0 0); + --border: oklch(1 0.5 100); + --input: oklch(0 0 0); + --ring: oklch(1 0.5 100); + --sidebar: oklch(0 0 0); + --sidebar-foreground: oklch(1 0.5 100); + --sidebar-primary: oklch(1 0.5 100); + --sidebar-primary-foreground: oklch(0 0 0); + --sidebar-accent: oklch(0 0 0); + --sidebar-accent-foreground: oklch(1 0.5 100); + --sidebar-border: oklch(1 0.5 100); + --sidebar-ring: oklch(1 0.5 100); } @theme inline { @@ -153,4 +151,4 @@ @apply bg-background text-foreground; transition: background-color 0.3s, color 0.3s; } -} \ No newline at end of file +} diff --git a/app/login/page.tsx b/app/login/page.tsx index b193142..4af96d9 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,82 +1,260 @@ // Caminho: app/login/page.tsx +"use client"; + +import { usersService } from "@/services/usersApi.mjs"; import { LoginForm } from "@/components/LoginForm"; import Link from "next/link"; import Image from "next/image"; import { Button } from "@/components/ui/button"; -import { ArrowLeft } from "lucide-react"; // Importa o ícone de seta +import { Input } from "@/components/ui/input"; +import { ArrowLeft, X } from "lucide-react"; +import { useState } from "react"; +import RenderFromTemplateContext from "next/dist/client/components/render-from-template-context"; export default function LoginPage() { - return ( -
- - {/* PAINEL ESQUERDO: O Formulário */} -
- - {/* Link para Voltar */} -
- - - Voltar à página inicial - -
+ const [isModalOpen, setIsModalOpen] = useState(false); + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [message, setMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); - {/* O contêiner principal que agora terá a sombra e o estilo de card */} -
-
-

Acesse sua conta

-

Bem-vindo(a) de volta ao MedConnect!

+ const handleOpenModal = () => { + // Tenta pegar o email do input do formulário de login + const emailInput = document.querySelector( + 'input[type="email"]' + ) as HTMLInputElement; + if (emailInput?.value) { + setEmail(emailInput.value); + } + setIsModalOpen(true); + }; + + const handleResetPassword = async () => { + if (!email.trim()) { + setMessage({ + type: "error", + text: "Por favor, insira um e-mail válido.", + }); + return; + } + + setIsLoading(true); + setMessage(null); + + try { + // Chama o método que já faz o fetch corretamente + const data = await usersService.resetPassword(email); + + console.log("Resposta resetPassword:", data); + + setMessage({ + type: "success", + text: "E-mail de recuperação enviado! Verifique sua caixa de entrada.", + }); + + setTimeout(() => { + setIsModalOpen(false); + setMessage(null); + setEmail(""); + }, 2000); + } catch (error) { + console.error("Erro no reset de senha:", error); + setMessage({ + type: "error", + text: + error instanceof Error + ? error.message + : "Erro ao enviar e-mail. Tente novamente.", + }); + } finally { + setIsLoading(false); + } + }; + + const closeModal = () => { + setIsModalOpen(false); + setMessage(null); + setEmail(""); + }; + + return ( + <> +
+ {/* PAINEL ESQUERDO: O Formulário */} +
+ {/* Link para Voltar */} +
+ + + Voltar à página inicial +
- - {/* Children para o LoginForm */} -
- - + {/* O contêiner principal que agora terá a sombra e o estilo de card */} +
+ {/* NOVO: Bloco da Logo e Nome (Painel Esquerdo) */} +
+ Logo MediConnect + + MedConnect + +
+ {/* FIM: Bloco da Logo e Nome */} + +
+ {/* Título de boas-vindas movido para baixo da logo */} +

+ Acesse sua conta +

+

+ Bem-vindo(a) de volta ao MedConnect! +

+
+ + + {/* Children para o LoginForm */} +
+ +
+
+ +
+ + Não tem uma conta de paciente?{" "} + + + + Crie uma agora
- +
+
-
- Não tem uma conta de paciente? - - - Crie uma agora - - + {/* PAINEL DIREITO: A Imagem e Branding */} +
+ {/* Usamos o componente para otimização e performance */} + Médica utilizando um tablet na clínica MedConnect + {/* Camada de sobreposição para escurecer a imagem e destacar o texto */} +
+ {/* BLOCO DE NOME ADICIONADO */} +
+

+ MedConnect +

+
+

+ Tecnologia e Cuidado a Serviço da Sua Saúde. +

+

+ Acesse seu portal para uma experiência de saúde integrada, segura + e eficiente. +

- {/* PAINEL DIREITO: A Imagem e Branding */} -
- {/* Usamos o componente para otimização e performance */} - Médica utilizando um tablet na clínica MedConnect - {/* Camada de sobreposição para escurecer a imagem e destacar o texto */} -
- {/* BLOCO DE NOME ADICIONADO */} -
-

- MedConnect -

-
-

- Tecnologia e Cuidado a Serviço da Sua Saúde. -

-

- Acesse seu portal para uma experiência de saúde integrada, segura e eficiente. -

-
-
+ {/* Modal de Recuperação de Senha */} + {isModalOpen && ( +
+
+ {/* Botão de fechar */} + -
+ {/* Cabeçalho */} +
+

+ Recuperar Senha +

+

+ Insira seu e-mail e enviaremos um link para redefinir sua senha. +

+
+ + {/* Input de e-mail */} +
+
+ + setEmail(e.target.value)} + placeholder="seu@email.com" + disabled={isLoading} + className="w-full" + /> +
+ + {/* Mensagem de feedback */} + {message && ( +
+ {message.text} +
+ )} + + {/* Botões */} +
+ {/* Botão Cancelar – Azul contornado */} + + + {/* Botão Resetar Senha – Azul sólido */} + +
+
+
+
+ )} + ); -} \ No newline at end of file +} diff --git a/app/manager/dashboard/page.tsx b/app/manager/dashboard/page.tsx index df56541..11f5c19 100644 --- a/app/manager/dashboard/page.tsx +++ b/app/manager/dashboard/page.tsx @@ -1,190 +1,242 @@ "use client"; -import ManagerLayout from "@/components/manager-layout"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Calendar, Clock, Plus, User } from "lucide-react"; +import { Clock, Plus, User } from "lucide-react"; // Removi 'Calendar' que não estava sendo usado import Link from "next/link"; import React, { useState, useEffect } from "react"; import { usersService } from "services/usersApi.mjs"; import { doctorsService } from "services/doctorsApi.mjs"; +import Sidebar from "@/components/Sidebar"; +import { api } from "services/api.mjs"; // <-- ADICIONEI ESTE IMPORT export default function ManagerDashboard() { - // 🔹 Estados para usuários - const [firstUser, setFirstUser] = useState(null); - const [loadingUser, setLoadingUser] = useState(true); + // 🔹 Estados para usuários + const [firstUser, setFirstUser] = useState(null); + const [loadingUser, setLoadingUser] = useState(true); - // 🔹 Estados para médicos - const [doctors, setDoctors] = useState([]); - const [loadingDoctors, setLoadingDoctors] = useState(true); + // 🔹 Estados para médicos + const [doctors, setDoctors] = useState([]); + const [loadingDoctors, setLoadingDoctors] = useState(true); - // 🔹 Buscar primeiro usuário + // 🔹 Buscar primeiro usuário (LÓGICA ATUALIZADA) useEffect(() => { async function fetchFirstUser() { + setLoadingUser(true); // Garante que o estado de loading inicie como true try { - const data = await usersService.list_roles(); - if (Array.isArray(data) && data.length > 0) { - setFirstUser(data[0]); + // 1. Busca a lista de usuários com seus cargos (roles) + const rolesData = await usersService.list_roles(); + + // 2. Verifica se a lista não está vazia + if (Array.isArray(rolesData) && rolesData.length > 0) { + const firstUserRole = rolesData[0]; + const firstUserId = firstUserRole.user_id; + + if (!firstUserId) { + throw new Error("O primeiro usuário da lista não possui um ID válido."); + } + + // 3. Usa o ID para buscar o perfil (com nome e email) do usuário + const profileData = await api.get( + `/rest/v1/profiles?select=full_name,email&id=eq.${firstUserId}` + ); + + // 4. Verifica se o perfil foi encontrado + if (Array.isArray(profileData) && profileData.length > 0) { + const userProfile = profileData[0]; + // 5. Combina os dados do cargo e do perfil e atualiza o estado + setFirstUser({ + ...firstUserRole, + ...userProfile + }); + } else { + // Se não encontrar o perfil, exibe os dados que temos + setFirstUser(firstUserRole); + } } } catch (error) { console.error("Erro ao carregar usuário:", error); + setFirstUser(null); // Limpa o usuário em caso de erro } finally { setLoadingUser(false); } } - fetchFirstUser(); - }, []); + fetchFirstUser(); + }, []); - // 🔹 Buscar 3 primeiros médicos - useEffect(() => { - async function fetchDoctors() { - try { - const data = await doctorsService.list(); // ajuste se seu service tiver outro método - if (Array.isArray(data)) { - setDoctors(data.slice(0, 3)); // pega os 3 primeiros - } - } catch (error) { - console.error("Erro ao carregar médicos:", error); - } finally { - setLoadingDoctors(false); - } + // 🔹 Buscar 3 primeiros médicos + useEffect(() => { + async function fetchDoctors() { + try { + const data = await doctorsService.list(); // ajuste se seu service tiver outro método + if (Array.isArray(data)) { + setDoctors(data.slice(0, 3)); // pega os 3 primeiros } + } catch (error) { + console.error("Erro ao carregar médicos:", error); + } finally { + setLoadingDoctors(false); + } + } - fetchDoctors(); - }, []); + fetchDoctors(); + }, []); - return ( - -
- {/* Cabeçalho */} -
-

Dashboard

-

Bem-vindo ao seu portal de consultas médicas

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

Dashboard

+

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

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

Relatórios disponíveis

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

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

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

Dados completos

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

Carregando médicos...

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

Nenhum médico cadastrado.

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

{doc.full_name || "Sem nome"}

-

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

-
-
-

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

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

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

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

Dados completos

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

Carregando médicos...

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

+ Nenhum médico cadastrado. +

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

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

+

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

+
+
+

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

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

Disponibilidade dos Médicos

+

Visualize a agenda semanal individual de cada médico.

+
+ + + {/* 🔎 Filtros */} +
+ {/* Filtro por nome */} + + { + setSearch(e.target.value); + setPage(1); + }} + className="w-full md:w-1/3" + /> + + {/* Filtro por especialidade */} + +
+
+
+ {/* GRID de cards */} +
+ {paginatedDoctors.map((doctor) => { + const doctorAvailabilities = availabilities.filter((a) => a.doctor_id === doctor.id); + + return ( + + + {doctor.full_name} + + + + + + + ); + })} +
+ + {/* 📄 Paginação */} + {totalPages > 1 && ( +
+ + + + Página {page} de {totalPages} + + + +
+ )} +
+
+ ); +} diff --git a/app/manager/home/[id]/editar/page.tsx b/app/manager/home/[id]/editar/page.tsx index 6619f67..b3bfeea 100644 --- a/app/manager/home/[id]/editar/page.tsx +++ b/app/manager/home/[id]/editar/page.tsx @@ -9,47 +9,47 @@ import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" -import { Save, Loader2, ArrowLeft } from "lucide-react" -import ManagerLayout from "@/components/manager-layout" -import { doctorsService } from "services/doctorsApi.mjs"; +import { Save, Loader2, ArrowLeft } from "lucide-react" +import Sidebar from "@/components/Sidebar" +import { doctorsService } from "services/doctorsApi.mjs"; const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"]; interface DoctorFormData { - nomeCompleto: string; - crm: string; - crmEstado: string; - especialidade: string; - cpf: string; - email: string; - dataNascimento: string; - rg: string; - telefoneCelular: string; - telefone2: string; - cep: string; - endereco: string; - numero: string; - complemento: string; - bairro: string; - cidade: string; - estado: string; - ativo: boolean; - observacoes: string; + nomeCompleto: string; + crm: string; + crmEstado: string; + especialidade: string; + cpf: string; + email: string; + dataNascimento: string; + rg: string; + telefoneCelular: string; + telefone2: string; + cep: string; + endereco: string; + numero: string; + complemento: string; + bairro: string; + cidade: string; + estado: string; + ativo: boolean; + observacoes: string; } const apiMap: { [K in keyof DoctorFormData]: string | null } = { - nomeCompleto: 'full_name', crm: 'crm', crmEstado: 'crm_uf', especialidade: 'specialty', - cpf: 'cpf', email: 'email', dataNascimento: 'birth_date', rg: 'rg', - telefoneCelular: 'phone_mobile', telefone2: 'phone2', cep: 'cep', - endereco: 'street', numero: 'number', complemento: 'complement', - bairro: 'neighborhood', cidade: 'city', estado: 'state', ativo: 'active', - observacoes: null, + nomeCompleto: 'full_name', crm: 'crm', crmEstado: 'crm_uf', especialidade: 'specialty', + cpf: 'cpf', email: 'email', dataNascimento: 'birth_date', rg: 'rg', + telefoneCelular: 'phone_mobile', telefone2: 'phone2', cep: 'cep', + endereco: 'street', numero: 'number', complemento: 'complement', + bairro: 'neighborhood', cidade: 'city', estado: 'state', ativo: 'active', + observacoes: null, }; const defaultFormData: DoctorFormData = { - nomeCompleto: '', crm: '', crmEstado: '', especialidade: '', cpf: '', email: '', - dataNascimento: '', rg: '', telefoneCelular: '', telefone2: '', cep: '', - endereco: '', numero: '', complemento: '', bairro: '', cidade: '', estado: '', - ativo: true, observacoes: '', + nomeCompleto: '', crm: '', crmEstado: '', especialidade: '', cpf: '', email: '', + dataNascimento: '', rg: '', telefoneCelular: '', telefone2: '', cep: '', + endereco: '', numero: '', complemento: '', bairro: '', cidade: '', estado: '', + ativo: true, observacoes: '', }; const cleanNumber = (value: string): string => value.replace(/\D/g, ''); @@ -73,420 +73,420 @@ const formatPhoneMobile = (value: string): string => { }; export default function EditarMedicoPage() { - const router = useRouter(); - const params = useParams(); - const id = Array.isArray(params.id) ? params.id[0] : params.id; - const [formData, setFormData] = useState(defaultFormData); - const [loading, setLoading] = useState(true); - const [isSaving, setIsSaving] = useState(false); - const [error, setError] = useState(null); - const apiToFormMap: { [key: string]: keyof DoctorFormData } = { - 'full_name': 'nomeCompleto', 'crm': 'crm', 'crm_uf': 'crmEstado', 'specialty': 'especialidade', - 'cpf': 'cpf', 'email': 'email', 'birth_date': 'dataNascimento', 'rg': 'rg', - 'phone_mobile': 'telefoneCelular', 'phone2': 'telefone2', 'cep': 'cep', - 'street': 'endereco', 'number': 'numero', 'complement': 'complemento', - 'neighborhood': 'bairro', 'city': 'cidade', 'state': 'estado', 'active': 'ativo' - }; - - - useEffect(() => { - if (!id) return; - - const fetchDoctor = async () => { - try { - const data = await doctorsService.getById(id); - - if (!data) { - setError("Médico não encontrado."); - setLoading(false); - return; - } - - const initialData: Partial = {}; - - Object.keys(data).forEach(key => { - const formKey = apiToFormMap[key]; - if (formKey) { - let value = data[key] === null ? '' : data[key]; - if (formKey === 'ativo') { - value = !!value; - } else if (typeof value !== 'boolean') { - value = String(value); - } - initialData[formKey] = value as any; - } - }); - initialData.observacoes = "Observação carregada do sistema (exemplo de campo interno)"; - - setFormData(prev => ({ ...prev, ...initialData })); - } catch (e) { - console.error("Erro ao carregar dados:", e); - setError("Não foi possível carregar os dados do médico."); - } finally { - setLoading(false); - } + const router = useRouter(); + const params = useParams(); + const id = Array.isArray(params.id) ? params.id[0] : params.id; + const [formData, setFormData] = useState(defaultFormData); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const apiToFormMap: { [key: string]: keyof DoctorFormData } = { + 'full_name': 'nomeCompleto', 'crm': 'crm', 'crm_uf': 'crmEstado', 'specialty': 'especialidade', + 'cpf': 'cpf', 'email': 'email', 'birth_date': 'dataNascimento', 'rg': 'rg', + 'phone_mobile': 'telefoneCelular', 'phone2': 'telefone2', 'cep': 'cep', + 'street': 'endereco', 'number': 'numero', 'complement': 'complemento', + 'neighborhood': 'bairro', 'city': 'cidade', 'state': 'estado', 'active': 'ativo' }; - fetchDoctor(); - }, [id]); - - const handleInputChange = (key: keyof DoctorFormData, value: string | boolean) => { - - - if (typeof value === 'string') { - let maskedValue = value; - if (key === 'cpf') maskedValue = formatCPF(value); - if (key === 'cep') maskedValue = formatCEP(value); - if (key === 'telefoneCelular' || key === 'telefone2') maskedValue = formatPhoneMobile(value); - - setFormData((prev) => ({ ...prev, [key]: maskedValue })); - } else { - setFormData((prev) => ({ ...prev, [key]: value })); - } - }; + useEffect(() => { + if (!id) return; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - setIsSaving(true); - - if (!id) { - setError("ID do médico ausente."); - setIsSaving(false); - return; - } + const fetchDoctor = async () => { + try { + const data = await doctorsService.getById(id); - const finalPayload: { [key: string]: any } = {}; - const formKeys = Object.keys(formData) as Array; + if (!data) { + setError("Médico não encontrado."); + setLoading(false); + return; + } - - formKeys.forEach((key) => { - const apiFieldName = apiMap[key]; - - if (!apiFieldName) return; + const initialData: Partial = {}; + + Object.keys(data).forEach(key => { + const formKey = apiToFormMap[key]; + if (formKey) { + let value = data[key] === null ? '' : data[key]; + if (formKey === 'ativo') { + value = !!value; + } else if (typeof value !== 'boolean') { + value = String(value); + } + initialData[formKey] = value as any; + } + }); + initialData.observacoes = "Observação carregada do sistema (exemplo de campo interno)"; + + setFormData(prev => ({ ...prev, ...initialData })); + } catch (e) { + console.error("Erro ao carregar dados:", e); + setError("Não foi possível carregar os dados do médico."); + } finally { + setLoading(false); + } + }; + fetchDoctor(); + }, [id]); + + const handleInputChange = (key: keyof DoctorFormData, value: string | boolean) => { - let value = formData[key]; if (typeof value === 'string') { - let trimmedValue = value.trim(); - if (trimmedValue === '') { - finalPayload[apiFieldName] = null; - return; - } - if (key === 'crmEstado' || key === 'estado') { - trimmedValue = trimmedValue.toUpperCase(); - } - - value = trimmedValue; + let maskedValue = value; + if (key === 'cpf') maskedValue = formatCPF(value); + if (key === 'cep') maskedValue = formatCEP(value); + if (key === 'telefoneCelular' || key === 'telefone2') maskedValue = formatPhoneMobile(value); + + setFormData((prev) => ({ ...prev, [key]: maskedValue })); + } else { + setFormData((prev) => ({ ...prev, [key]: value })); } - - finalPayload[apiFieldName] = value; - }); + }; - delete finalPayload.user_id; - try { - await doctorsService.update(id, finalPayload); - router.push("/manager/home"); - } catch (e: any) { - console.error("Erro ao salvar o médico:", e); - let detailedError = "Erro ao atualizar. Verifique os dados e tente novamente."; - - if (e.message && e.message.includes("duplicate key value violates unique constraint")) { - detailedError = "O CPF ou CRM informado já está cadastrado em outro registro."; - } else if (e.message && e.message.includes("Detalhes:")) { - detailedError = e.message.split("Detalhes:")[1].trim(); - } else if (e.message) { - detailedError = e.message; - } - - setError(`Erro ao atualizar. Detalhes: ${detailedError}`); - } finally { - setIsSaving(false); - } - }; - if (loading) { - return ( - -
- -

Carregando dados do médico...

-
-
- ); - } - return ( - -
-
-
-

- Editar Médico: {formData.nomeCompleto} -

-

- Atualize as informações do médico (ID: {id}). -

-
- - - -
-
- - {error && ( -
-

Erro na Atualização:

-

{error}

-
- )} + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsSaving(true); -
-

- Dados Principais e Pessoais -

-
-
- - handleInputChange("nomeCompleto", e.target.value)} - placeholder="Nome do Médico" - /> -
-
- - handleInputChange("crm", e.target.value)} - placeholder="Ex: 123456" - /> -
-
- - -
-
- - -
-
- - handleInputChange("especialidade", e.target.value)} - placeholder="Ex: Cardiologia" - /> -
-
- - handleInputChange("cpf", e.target.value)} - placeholder="000.000.000-00" - maxLength={14} - /> -
-
- - handleInputChange("rg", e.target.value)} - placeholder="00.000.000-0" - /> -
-
- -
-
- - handleInputChange("email", e.target.value)} - placeholder="exemplo@dominio.com" - /> -
-
- - handleInputChange("dataNascimento", e.target.value)} - /> -
-
-
- handleInputChange("ativo", checked === true)} - /> - + if (!id) { + setError("ID do médico ausente."); + setIsSaving(false); + return; + } + + const finalPayload: { [key: string]: any } = {}; + const formKeys = Object.keys(formData) as Array; + + + formKeys.forEach((key) => { + const apiFieldName = apiMap[key]; + + if (!apiFieldName) return; + + let value = formData[key]; + + if (typeof value === 'string') { + let trimmedValue = value.trim(); + if (trimmedValue === '') { + finalPayload[apiFieldName] = null; + return; + } + if (key === 'crmEstado' || key === 'estado') { + trimmedValue = trimmedValue.toUpperCase(); + } + + value = trimmedValue; + } + + finalPayload[apiFieldName] = value; + }); + + delete finalPayload.user_id; + try { + await doctorsService.update(id, finalPayload); + router.push("/manager/home"); + } catch (e: any) { + console.error("Erro ao salvar o médico:", e); + let detailedError = "Erro ao atualizar. Verifique os dados e tente novamente."; + + if (e.message && e.message.includes("duplicate key value violates unique constraint")) { + detailedError = "O CPF ou CRM informado já está cadastrado em outro registro."; + } else if (e.message && e.message.includes("Detalhes:")) { + detailedError = e.message.split("Detalhes:")[1].trim(); + } else if (e.message) { + detailedError = e.message; + } + + setError(`Erro ao atualizar. Detalhes: ${detailedError}`); + } finally { + setIsSaving(false); + } + }; + if (loading) { + return ( + +
+ +

Carregando dados do médico...

-
-
-
+ + ); + } -
-

- Contato e Endereço -

- -
-
- - handleInputChange("telefoneCelular", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
- - handleInputChange("telefone2", e.target.value)} - placeholder="(00) 00000-0000" - maxLength={15} - /> -
-
+ return ( + +
+
+
+

+ Editar Médico: {formData.nomeCompleto} +

+

+ Atualize as informações do médico +

+
+ + + +
- -
-
- - handleInputChange("cep", e.target.value)} - placeholder="00000-000" - maxLength={9} - /> -
-
- - handleInputChange("endereco", e.target.value)} - placeholder="Rua, Avenida, etc." - /> -
-
- -
-
- - handleInputChange("numero", e.target.value)} - placeholder="123" - /> -
-
- - handleInputChange("complemento", e.target.value)} - placeholder="Apto, Bloco, etc." - /> -
-
- -
-
- - handleInputChange("bairro", e.target.value)} - placeholder="Bairro" - /> -
-
- - handleInputChange("cidade", e.target.value)} - placeholder="São Paulo" - /> -
-
- - handleInputChange("estado", e.target.value)} - placeholder="SP" - /> -
-
-
+ - -
-

- Observações (Apenas internas) -

-