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

Dr. João Santos

-

Cardiologia

-
-
-

02 out

-

14:30

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

Dr. João Santos

+

Cardiologia

+
+
+

02 out

+

14:30

+
+
+
+
+
-
-
-
-
-
- - - Horário Semanal - - Confira rapidamente a sua disponibilidade da semana - - - - {[ - "sunday", - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - ].map((day) => { - const times = schedule[day] || []; - return ( -
-
-
-

- {weekdaysPT[day]} -

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

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

- )) - ) : ( -

- Sem horário -

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

{date}

-

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

-
-
-

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

-

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

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

- Nenhuma exceção registrada. -

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

{date}

+

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

+
+
+

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

+

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

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

Nenhuma exceção registrada.

+ )} + + +
+ + + + Confirmar exclusão + Tem certeza que deseja excluir esta exceção? Esta ação não pode ser desfeita. + + + Cancelar + exceptionToDelete && handleDeleteException(exceptionToDelete)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + + + + + ); } diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index c4947e4..7fd25fd 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -218,36 +218,36 @@ export default function AvailabilityPage() { } }; - // Mapa de tradução - const weekdaysPT: Record = { - sunday: "Domingo", - monday: "Segunda", - tuesday: "Terça", - wednesday: "Quarta", - thursday: "Quinta", - friday: "Sexta", - saturday: "Sábado", - }; - const fetchData = async () => { - try { - const loggedUser = await usersService.getMe(); - const doctorList = await doctorsService.list(); - setUserData(loggedUser); - const doctor = findDoctorById(loggedUser.user.id, doctorList); - setDoctorId(doctor?.id); - console.log(doctor); - // Busca disponibilidade - const availabilityList = await AvailabilityService.list(); - - // Filtra já com a variável local - const filteredAvail = availabilityList.filter( - (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id - ); - setAvailability(filteredAvail); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; + // Mapa de tradução + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", + }; + const fetchData = async () => { + try { + const loggedUser = await usersService.getMe(); + const doctorList = await doctorsService.list(); + setUserData(loggedUser); + const doctor = findDoctorById(loggedUser.user.id, doctorList); + setDoctorId(doctor?.id); + console.log(doctor); + // Busca disponibilidade + const availabilityList = await AvailabilityService.list(); + + // Filtra já com a variável local + const filteredAvail = availabilityList.filter( + (disp: { doctor_id: string }) => disp.doctor_id === doctor?.id + ); + setAvailability(filteredAvail); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; useEffect(() => { fetchData(); @@ -320,20 +320,21 @@ export default function AvailabilityPage() { } } catch {} - toast({ - title: "Sucesso", - description: message, - }); - router.push("#"); // adicionar página para listar a disponibilidade - } catch (err: any) { - toast({ - title: "Erro", - description: err?.message || "Não foi possível criar a disponibilidade", - }); - } finally { - setIsLoading(false); - } - }; + toast({ + title: "Sucesso", + description: message, + }); + router.push("#"); // adicionar página para listar a disponibilidade + } catch (err: any) { + toast({ + title: "Erro", + description: err?.message || "Não foi possível criar a disponibilidade", + }); + } finally { + fetchData() + setIsLoading(false); + } + }; const openDeleteDialog = ( schedule: { start: string; end: string }, @@ -343,38 +344,35 @@ export default function AvailabilityPage() { setDeleteDialogOpen(true); }; - const handleDeleteAvailability = async (AvailabilityId: string) => { - try { - const res = await AvailabilityService.delete(AvailabilityId); - - let message = "Disponibilidade deletada com sucesso"; - try { - if (res) { - throw new Error( - `${res.error} ${res.message}` || "A API retornou erro" - ); - } else { - console.log(message); - } - } catch {} - - toast({ - title: "Sucesso", - description: message, - }); - - setAvailability((prev: Availability[]) => - prev.filter((p) => String(p.id) !== String(AvailabilityId)) - ); - } catch (e: any) { - toast({ - title: "Erro", - description: e?.message || "Não foi possível deletar a disponibilidade", - }); - } - setDeleteDialogOpen(false); - setSelectedAvailability(null); - }; + const handleDeleteAvailability = async (AvailabilityId: string) => { + try { + const res = await AvailabilityService.delete(AvailabilityId); + + let message = "Disponibilidade deletada com sucesso"; + try { + if (res) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + + setAvailability((prev: Availability[]) => prev.filter((p) => String(p.id) !== String(AvailabilityId))); + } catch (e: any) { + toast({ + title: "Erro", + description: e?.message || "Não foi possível deletar a disponibilidade", + }); + } + fetchData() + setDeleteDialogOpen(false); + setSelectedAvailability(null); + }; return ( @@ -542,142 +540,99 @@ export default function AvailabilityPage() { - {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} - {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} - {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */} -
- - - -
- {" "} - {/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */} - - - - -
-
- - - {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */} -
- - - Horário Semanal - - Confira ou altere a sua disponibilidade da semana - - - {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */} - - {[ - "sunday", - "monday", - "tuesday", - "wednesday", - "thursday", - "friday", - "saturday", - ].map((day) => { - const times = schedule[day] || []; - return ( -
-
-

- {weekdaysPT[day]} -

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

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

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

- Sem horário -

- )} -
+ {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */} + {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */} + {/* Alteração aqui: Adicionado w-full aos Links e Buttons para ocuparem a largura total em telas pequenas */} +
+ + + +
{/* Ajustado para empilhar os botões Cancelar e Salvar em telas pequenas */} + + + + +
-
- ); - })} - - -
- - {/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} - - - - Confirmar exclusão - - Tem certeza que deseja excluir esta disponibilidade? Esta ação - não pode ser desfeita. - - - - Cancelar - - selectedAvailability && - handleDeleteAvailability(selectedAvailability.id) - } - className="bg-red-600 hover:bg-red-700" - > - Excluir - - - - -
- -
- ); + + + {/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */} +
+ + + Horário Semanal + Confira ou altere a sua disponibilidade da semana + + {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */} + + {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { + const times = schedule[day] || []; + return ( +
+
+

{weekdaysPT[day]}

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

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

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

Sem horário

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

Disponibilidade dos Médicos

+

Visualize a agenda semanal individual de cada médico.

+
+ + + {/* 🔎 Filtros */} +
+ {/* Filtro por nome */} + + { + setSearch(e.target.value); + setPage(1); + }} + className="w-full md:w-1/3" + /> + + {/* Filtro por especialidade */} + +
+
+
+ {/* GRID de cards */} +
+ {paginatedDoctors.map((doctor) => { + const doctorAvailabilities = availabilities.filter((a) => a.doctor_id === doctor.id); + + return ( + + + {doctor.full_name} + + + + + + + ); + })} +
+ + {/* 📄 Paginação */} + {totalPages > 1 && ( +
+ + + + Página {page} de {totalPages} + + + +
+ )} +
+
+ ); +} diff --git a/app/manager/home/page.tsx b/app/manager/home/page.tsx index 55c0eff..614df35 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -4,34 +4,19 @@ import React, { useEffect, useState, useCallback, useMemo } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Edit, Trash2, Eye, Calendar, Loader2 } from "lucide-react"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { doctorsService } from "services/doctorsApi.mjs"; +// Imports dos Serviços +import { doctorsService } from "@/services/doctorsApi.mjs"; import Sidebar from "@/components/Sidebar"; +// --- NOVOS IMPORTS (Certifique-se que criou os arquivos no passo anterior) --- +import { FilterBar } from "@/components/ui/filter-bar"; +import { normalizeSpecialty, getUniqueSpecialties } from "@/lib/normalization"; + interface Doctor { id: number; full_name: string; @@ -66,119 +51,111 @@ interface DoctorDetails { export default function DoctorsPage() { const router = useRouter(); - const [doctors, setDoctors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [doctorDetails, setDoctorDetails] = useState( - null - ); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); + // --- Estados de Dados --- + const [doctors, setDoctors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // --- Estados para Filtros --- - const [specialtyFilter, setSpecialtyFilter] = useState("all"); - const [statusFilter, setStatusFilter] = useState("all"); + // --- Estados de Modais --- + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [doctorDetails, setDoctorDetails] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); - // --- Estados para Paginação --- - const [itemsPerPage, setItemsPerPage] = useState(10); - const [currentPage, setCurrentPage] = useState(1); + // --- Estados de Filtro e Busca --- + const [searchTerm, setSearchTerm] = useState(""); + const [filters, setFilters] = useState({ + specialty: "all", + status: "all" + }); - const fetchDoctors = useCallback(async () => { - setLoading(true); - setError(null); - try { - const data: Doctor[] = await doctorsService.list(); - const dataWithStatus = data.map((doc, index) => ({ - ...doc, - status: - index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo", - })); - setDoctors(dataWithStatus || []); - setCurrentPage(1); - } catch (e: any) { - console.error("Erro ao carregar lista de médicos:", e); - setError( - "Não foi possível carregar a lista de médicos. Verifique a conexão com a API." - ); - setDoctors([]); - } finally { - setLoading(false); - } - }, []); + // --- Estados de Paginação --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + + // 1. Buscar Médicos na API + const fetchDoctors = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data: Doctor[] = await doctorsService.list(); + // Mockando status para visualização (conforme original) + const dataWithStatus = data.map((doc, index) => ({ + ...doc, + status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo", + })); + setDoctors(dataWithStatus || []); + // Não resetamos a página aqui para manter a navegação fluida se apenas recarregar dados + } catch (e: any) { + console.error("Erro ao carregar lista de médicos:", e); + setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); + setDoctors([]); + } finally { + setLoading(false); + } + }, []); useEffect(() => { fetchDoctors(); }, [fetchDoctors]); - const openDetailsDialog = async (doctor: Doctor) => { - setDetailsDialogOpen(true); - setDoctorDetails({ - nome: doctor.full_name, - crm: doctor.crm, - especialidade: doctor.specialty, - contato: { celular: doctor.phone_mobile ?? undefined }, - endereco: { - cidade: doctor.city ?? undefined, - estado: doctor.state ?? undefined, - }, - status: doctor.status || "Ativo", - convenio: "Particular", - vip: false, - ultimo_atendimento: "N/A", - proximo_atendimento: "N/A", - }); - }; + // 2. Gerar lista única de especialidades (Normalizada) + const uniqueSpecialties = useMemo(() => { + return getUniqueSpecialties(doctors); + }, [doctors]); - const handleDelete = async () => { - if (doctorToDeleteId === null) return; - setLoading(true); - try { - await doctorsService.delete(doctorToDeleteId); - setDeleteDialogOpen(false); - setDoctorToDeleteId(null); - await fetchDoctors(); - } catch (e) { - console.error("Erro ao excluir:", e); - alert("Erro ao excluir médico."); - } finally { - setLoading(false); - } - }; + // 3. Lógica de Filtragem Centralizada + const filteredDoctors = useMemo(() => { + return doctors.filter((doctor) => { + // Normaliza a especialidade do médico atual para comparar + const normalizedDocSpecialty = normalizeSpecialty(doctor.specialty); + + // Filtros exatos + const specialtyMatch = filters.specialty === "all" || normalizedDocSpecialty === filters.specialty; + const statusMatch = filters.status === "all" || doctor.status === filters.status; + + // Busca textual (Nome, Telefone, CRM) + const searchLower = searchTerm.toLowerCase(); + const nameMatch = doctor.full_name?.toLowerCase().includes(searchLower); + const phoneMatch = doctor.phone_mobile?.includes(searchLower); + const crmMatch = doctor.crm?.toLowerCase().includes(searchLower); - const openDeleteDialog = (doctorId: number) => { - setDoctorToDeleteId(doctorId); - setDeleteDialogOpen(true); - }; + return specialtyMatch && statusMatch && (searchTerm === "" || nameMatch || phoneMatch || crmMatch); + }); + }, [doctors, filters, searchTerm]); - const uniqueSpecialties = useMemo(() => { - const specialties = doctors - .map((doctor) => doctor.specialty) - .filter(Boolean); - return [...new Set(specialties)]; - }, [doctors]); + // --- Handlers de Controle (Com Reset de Paginação) --- - const filteredDoctors = doctors.filter((doctor) => { - const specialtyMatch = - specialtyFilter === "all" || doctor.specialty === specialtyFilter; - const statusMatch = - statusFilter === "all" || doctor.status === statusFilter; - return specialtyMatch && statusMatch; - }); + const handleSearch = (term: string) => { + setSearchTerm(term); + setCurrentPage(1); // Correção: Reseta para página 1 ao buscar + }; - const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + const handleFilterChange = (key: string, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); + setCurrentPage(1); // Correção: Reseta para página 1 ao filtrar + }; - const goToPrevPage = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; + const handleClearFilters = () => { + setSearchTerm(""); + setFilters({ specialty: "all", status: "all" }); + setCurrentPage(1); // Correção: Reseta para página 1 ao limpar + }; - const goToNextPage = () => { - setCurrentPage((prev) => Math.min(totalPages, prev + 1)); - }; + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + + // --- Lógica de Paginação --- + const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); + + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + const goToPrevPage = () => setCurrentPage((prev) => Math.max(1, prev - 1)); + const goToNextPage = () => setCurrentPage((prev) => Math.min(totalPages, prev + 1)); const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { const pages: number[] = []; @@ -204,10 +181,43 @@ export default function DoctorsPage() { const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(Number(value)); - setCurrentPage(1); - }; + // --- Handlers de Ações (Detalhes e Delete) --- + const openDetailsDialog = (doctor: Doctor) => { + setDetailsDialogOpen(true); + setDoctorDetails({ + nome: doctor.full_name, + crm: doctor.crm, + especialidade: normalizeSpecialty(doctor.specialty), // Exibe normalizado + contato: { celular: doctor.phone_mobile ?? undefined }, + endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, + status: doctor.status || "Ativo", + convenio: "Particular", + vip: false, + ultimo_atendimento: "N/A", + proximo_atendimento: "N/A", + }); + }; + + const openDeleteDialog = (doctorId: number) => { + setDoctorToDeleteId(doctorId); + setDeleteDialogOpen(true); + }; + + const handleDelete = async () => { + if (doctorToDeleteId === null) return; + setLoading(true); + try { + await doctorsService.delete(doctorToDeleteId); + setDeleteDialogOpen(false); + setDoctorToDeleteId(null); + await fetchDoctors(); + } catch (e) { + console.error("Erro ao excluir:", e); + alert("Erro ao excluir médico."); + } finally { + setLoading(false); + } + }; return ( @@ -224,257 +234,192 @@ export default function DoctorsPage() { - {/* Filtros e Itens por Página */} -
-
- - Especialidade - - -
-
- Status - -
-
- - Itens por página - - -
- -
- - {/* Tabela de Médicos (Visível em Telas Médias e Maiores) */} -
- {loading ? ( -
- - Carregando médicos... -
- ) : error ? ( -
{error}
- ) : filteredDoctors.length === 0 ? ( -
- {doctors.length === 0 ? ( - <> - Nenhum médico cadastrado.{" "} - - Adicione um novo - - . - - ) : ( - "Nenhum médico encontrado com os filtros aplicados." - )} -
- ) : ( -
- - - - - - - - - - - - - {currentItems.map((doctor) => ( - - - - - - - - - ))} - -
- Nome - - CRM - - Especialidade - - Status - - Cidade/Estado - - Ações -
- {doctor.full_name} - - {doctor.crm} - - {doctor.specialty} - - {doctor.status || "N/A"} - - {doctor.city || doctor.state - ? `${doctor.city || ""}${ - doctor.city && doctor.state ? "/" : "" - }${doctor.state || ""}` - : "N/A"} - - - -
- Ações -
-
- - openDetailsDialog(doctor)} - > - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(doctor.id)} - > - - Excluir - - -
-
-
- )} -
- - {/* Cards de Médicos (Visível Apenas em Telas Pequenas) */} -
- {loading ? ( -
- - Carregando médicos... -
- ) : error ? ( -
{error}
- ) : filteredDoctors.length === 0 ? ( -
- {doctors.length === 0 ? ( - <> - Nenhum médico cadastrado.{" "} - - Adicione um novo - - . - - ) : ( - "Nenhum médico encontrado com os filtros aplicados." - )} -
- ) : ( -
- {currentItems.map((doctor) => ( -
-
-
- {doctor.full_name} + {/* Seletor de Itens por Página (Filho do FilterBar) */} +
+
-
- {doctor.specialty} -
-
- - -
- Ações -
-
- - openDetailsDialog(doctor)} - > - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(doctor.id)} - > - - Excluir - - -
+ + + {/* Tabela de Médicos */} +
+ {loading ? ( +
+ + Carregando médicos... +
+ ) : error ? ( +
{error}
+ ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 + ? <>Nenhum médico cadastrado. Adicione um novo. + : "Nenhum médico encontrado com os filtros aplicados." + } +
+ ) : ( +
+ + + + + + + + + + + + + {currentItems.map((doctor) => ( + + + + + + + + + ))} + +
NomeCRMEspecialidadeStatusCidade/EstadoAções
+ {doctor.full_name} + {doctor.crm} + {/* Exibe Especialidade Normalizada */} + {normalizeSpecialty(doctor.specialty)} + + + {doctor.status || "N/A"} + + + {(doctor.city || doctor.state) + ? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}` + : "N/A"} + + + +
Ações
+
+ + openDetailsDialog(doctor)}> + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + openDeleteDialog(doctor.id)}> + + Excluir + + +
+
+
+ )} +
+ + {/* Cards de Médicos (Mobile) */} +
+ {loading ? ( +
+ + Carregando médicos... +
+ ) : error ? ( +
{error}
+ ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 + ? <>Nenhum médico cadastrado. Adicione um novo. + : "Nenhum médico encontrado com os filtros aplicados." + } +
+ ) : ( +
+ {currentItems.map((doctor) => ( +
+
+
{doctor.full_name}
+
{doctor.phone_mobile}
+
{normalizeSpecialty(doctor.specialty)}
+
+ + {doctor.status || "N/A"} + +
+
+ + + + + + openDetailsDialog(doctor)}> + + Ver detalhes + + + + + Editar + + + openDeleteDialog(doctor.id)}> + + Excluir + + + +
+ ))} +
+ )}
- ))} -
- )} -
{/* Paginação */} {totalPages > 1 && ( @@ -511,31 +456,22 @@ export default function DoctorsPage() {
)} - {/* Dialogs de Exclusão e Detalhes */} - - - - Confirma a exclusão? - - Esta ação é irreversível e excluirá permanentemente o registro - deste médico. - - - - Cancelar - - {loading ? ( - - ) : null} - Excluir - - - - + {/* Dialogs (Exclusão e Detalhes) */} + + + + Confirma a exclusão? + Esta ação é irreversível e excluirá permanentemente o registro deste médico. + + + Cancelar + + {loading ? : null} + Excluir + + + + ([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [userDetails, setUserDetails] = useState(null); - const [selectedRole, setSelectedRole] = useState("all"); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [userDetails, setUserDetails] = useState(null); + + // --- Estados de Filtro --- + const [searchTerm, setSearchTerm] = useState(""); // <--- 2. Estado da busca + const [selectedRole, setSelectedRole] = useState("all"); // --- Lógica de Paginação INÍCIO --- const [itemsPerPage, setItemsPerPage] = useState(10); @@ -130,10 +120,21 @@ export default function UsersPage() { } }; - const filteredUsers = - selectedRole && selectedRole !== "all" - ? users.filter((u) => u.role === selectedRole) - : users; + // --- 3. Lógica de Filtragem Atualizada --- + const filteredUsers = users.filter((u) => { + // Filtro por Papel (Role) + const roleMatch = selectedRole === "all" || u.role === selectedRole; + + // Filtro da Barra de Pesquisa (Nome, Email ou Telefone) + const searchLower = searchTerm.toLowerCase(); + const nameMatch = u.full_name?.toLowerCase().includes(searchLower); + const emailMatch = u.email?.toLowerCase().includes(searchLower); + const phoneMatch = u.phone?.includes(searchLower); + + const searchMatch = !searchTerm || nameMatch || emailMatch || phoneMatch; + + return roleMatch && searchMatch; + }); const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; @@ -191,63 +192,71 @@ export default function UsersPage() {
- {/* Filtro e Itens por Página */} -
- {/* Select de Filtro por Papel - Ajustado para resetar a página */} -
- - Filtrar por papel - - -
+ {/* --- 4. Filtro (Barra de Pesquisa + Selects) --- */} +
- {/* Select de Itens por Página */} -
- - Itens por página - - -
- -
- {/* Fim do Filtro e Itens por Página */} + {/* Barra de Pesquisa */} +
+ + { + setSearchTerm(e.target.value); + setCurrentPage(1); // Reseta a paginação ao pesquisar + }} + className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors" + /> +
+ +
+ {/* Select de Filtro por Papel */} +
+ +
+ + {/* Select de Itens por Página */} +
+ +
+ + +
+
+ {/* Fim do Filtro */} {/* Tabela/Lista */}
@@ -315,34 +324,34 @@ export default function UsersPage() { - {/* Layout em Cards/Lista para Telas Pequenas */} -
- {currentItems.map((u) => ( -
-
-
- {u.full_name || "—"} -
-
- {u.role || "—"} -
-
-
- -
-
- ))} -
+ {/* Layout em Cards/Lista para Telas Pequenas */} +
+ {currentItems.map((u) => ( +
+
+
+ {u.full_name || "—"} +
+
+ {u.email} +
+
+ {u.role || "—"} +
+
+
+ +
+
+ ))} +
{/* Paginação */} {totalPages > 1 && ( diff --git a/app/secretary/pacientes/page.tsx b/app/secretary/pacientes/page.tsx index e91048c..fceeab4 100644 --- a/app/secretary/pacientes/page.tsx +++ b/app/secretary/pacientes/page.tsx @@ -227,31 +227,24 @@ export default function PacientesPage() {
- {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} -
- - VIP - - -
- - {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} - - + {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} +
+ VIP + +
+ + + + {/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} {/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */} diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index a43c9c1..aecf916 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -188,18 +188,14 @@ export default function Sidebar({ children }: SidebarProps) { }, ]; - const managerItems: MenuItem[] = [ - { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, - { href: "#", icon: ClipboardMinus, label: "Relatórios gerenciais" }, - { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, - { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, - { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, - { - href: "/secretary/appointments", - icon: CalendarCheck2, - label: "Consultas", - }, - ]; + const managerItems: MenuItem[] = [ + { href: "/manager/dashboard", icon: Home, label: "Dashboard" }, + { href: "/manager/usuario", icon: Users, label: "Gestão de Usuários" }, + { href: "/manager/home", icon: Stethoscope, label: "Gestão de Médicos" }, + { href: "/manager/pacientes", icon: Users, label: "Gestão de Pacientes" }, + { href: "/secretary/appointments", icon: CalendarCheck2, label: "Consultas" }, + { href: "/manager/disponibilidade", icon: ClipboardList, label: "Disponibilidade" }, + ]; switch (role) { case "gestor": diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index 257267e..8d0a0f4 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -19,9 +19,25 @@ import { import { Textarea } from "@/components/ui/textarea"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { format, addDays } from "date-fns"; -import { User, StickyNote, Calendar } from "lucide-react"; -import { smsService } from "@/services/Sms.mjs"; +import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react"; +import { smsService } from "@/services/Sms.mjs";; import { toast } from "@/hooks/use-toast"; +import { cn } from "@/lib/utils"; + +// Componentes do Combobox (Barra de Pesquisa) +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; export default function ScheduleForm() { // Estado do usuário e role @@ -31,8 +47,12 @@ export default function ScheduleForm() { // Listas e seleções const [patients, setPatients] = useState([]); const [selectedPatient, setSelectedPatient] = useState(""); + const [openPatientCombobox, setOpenPatientCombobox] = useState(false); + const [doctors, setDoctors] = useState([]); const [selectedDoctor, setSelectedDoctor] = useState(""); + const [openDoctorCombobox, setOpenDoctorCombobox] = useState(false); // Novo estado para médico + const [selectedDate, setSelectedDate] = useState(""); const [selectedTime, setSelectedTime] = useState(""); const [notes, setNotes] = useState(""); @@ -249,10 +269,7 @@ export default function ScheduleForm() { }, [selectedDoctor, selectedDate, fetchAvailableSlots]); // 🔹 Submeter agendamento - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento - // 🔹 Submeter agendamento + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -260,10 +277,7 @@ export default function ScheduleForm() { const patientId = isSecretaryLike ? selectedPatient : userId; if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { - toast({ - title: "Campos obrigatórios", - description: "Preencha todos os campos.", - }); + toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); return; } @@ -277,7 +291,6 @@ export default function ScheduleForm() { appointment_type: tipoConsulta, }; - // ✅ mantém o fluxo original de criação (funcional) await appointmentsService.create(body); const dateFormatted = selectedDate.split("-").reverse().join("/"); @@ -289,31 +302,20 @@ export default function ScheduleForm() { }.`, }); - let phoneNumber = "+5511999999999"; // fallback + let phoneNumber = "+5511999999999"; try { if (isSecretaryLike) { - // Secretária/admin → telefone do paciente selecionado const patient = patients.find((p: any) => p.id === patientId); - - // Pacientes criados no sistema podem ter phone ou phone_mobile const rawPhone = patient?.phone || patient?.phone_mobile || null; - if (rawPhone) phoneNumber = rawPhone; } else { - // Paciente → telefone vem do perfil do próprio usuário logado const me = await usersService.getMe(); - const rawPhone = me?.profile?.phone || - (typeof me?.profile === "object" && "phone_mobile" in me.profile - ? (me.profile as any).phone_mobile - : null) || - (typeof me === "object" && "user_metadata" in me - ? (me as any).user_metadata?.phone - : null) || + (typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || + (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || null; - if (rawPhone) phoneNumber = rawPhone; } @@ -397,63 +399,114 @@ export default function ScheduleForm() { Dados da Consulta -
-
- {/* Se secretária/gestor/admin → mostrar campo Paciente */} + +
{/* Ajuste: maior espaçamento vertical geral */} + + {/* Se secretária/gestor/admin → COMBOBOX de Paciente */} {["secretaria", "gestor", "admin"].includes(role) && ( -
+
{/* Ajuste: gap entre Label e Input */} - + + + + + + + + + Nenhum paciente encontrado. + + {patients.map((patient) => ( + { + setSelectedPatient(patient.id === selectedPatient ? "" : patient.id); + setOpenPatientCombobox(false); + }} + > + + {patient.full_name} + + ))} + + + + +
)} -
+ {/* COMBOBOX de Médico (Nova funcionalidade) */} +
{/* Ajuste: gap entre Label e Input */} - + + + + + + + + + Nenhum médico encontrado. + + {doctors.map((doctor) => ( + { + setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id); + setOpenDoctorCombobox(false); + }} + > + +
+ {doctor.full_name} + {doctor.specialty} +
+
+ ))} +
+
+
+
+
-
+
-
+