From 1aec6b56d0aa83ea24c95899b541250ad52f3129 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Wed, 5 Nov 2025 23:15:54 -0300 Subject: [PATCH] Aba de lista, e detalhes dos pacientes --- app/doctor/disponibilidade/page.tsx | 168 +++--- app/doctor/medicos/page.tsx | 323 ++++++++--- app/manager/home/page.tsx | 697 ++++++++++++++---------- app/manager/pacientes/page.tsx | 366 ++++++------- app/manager/usuario/page.tsx | 593 +++++++++++++------- app/secretary/pacientes/page.tsx | 818 +++++++++++++++------------- 6 files changed, 1734 insertions(+), 1231 deletions(-) diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 0e2cf70..36f0e98 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -21,6 +21,8 @@ import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; +// ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas) + interface UserPermissions { isAdmin: boolean; isManager: boolean; @@ -52,32 +54,32 @@ interface UserData { } 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 = { @@ -190,7 +192,7 @@ export default function AvailabilityPage() { 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 @@ -335,43 +337,45 @@ export default function AvailabilityPage() {

Dados

-
-
- -
- - - - - - - -
+ {/* **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 */}
@@ -409,53 +414,56 @@ 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 */} +
+ + -
- - +
+ + -
+ + {/* **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]}

-
-
+
+

{weekdaysPT[day]}

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

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

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

handleOpenModal(t, day)}> - + Editar openDeleteDialog(t, day)} - className="text-red-600"> + className="text-red-600 focus:bg-red-50 focus:text-red-600"> Excluir @@ -474,6 +482,8 @@ export default function AvailabilityPage() {
+ + {/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */} @@ -498,4 +508,4 @@ export default function AvailabilityPage() { ); -} +} \ No newline at end of file diff --git a/app/doctor/medicos/page.tsx b/app/doctor/medicos/page.tsx index 7e315c5..50db228 100644 --- a/app/doctor/medicos/page.tsx +++ b/app/doctor/medicos/page.tsx @@ -1,6 +1,7 @@ +// app/doctor/pacientes/page.tsx (assumindo a localização) "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import DoctorLayout from "@/components/doctor-layout"; import Link from "next/link"; import { @@ -9,9 +10,17 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; +import { Eye, Edit, Calendar, Trash2, Loader2 } from "lucide-react"; import { api } from "@/services/api.mjs"; import { PatientDetailsModal } from "@/components/ui/patient-details-modal"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Button } from "@/components/ui/button"; interface Paciente { id: string; @@ -41,6 +50,60 @@ export default function PacientesPage() { const [selectedPatient, setSelectedPatient] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); + // --- Lógica de Paginação INÍCIO --- + const [itemsPerPage, setItemsPerPage] = useState(5); + const [currentPage, setCurrentPage] = useState(1); + + const totalPages = Math.ceil(pacientes.length / itemsPerPage); + + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); + + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + // Funções de Navegação + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; + + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; + + // Lógica para gerar os números das páginas visíveis (máximo de 5) + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); + + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; + + const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + + // Lógica para mudar itens por página, resetando para a página 1 + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + // --- Lógica de Paginação FIM --- + + const handleOpenModal = (patient: Paciente) => { setSelectedPatient(patient); setIsModalOpen(true); @@ -51,92 +114,132 @@ export default function PacientesPage() { setIsModalOpen(false); }; - const formatDate = (dateString: string) => { - if (!dateString) return ""; - const date = new Date(dateString); - return new Intl.DateTimeFormat('pt-BR').format(date); + const formatDate = (dateString: string | null | undefined) => { + if (!dateString) return "N/A"; + try { + const date = new Date(dateString); + return new Intl.DateTimeFormat("pt-BR").format(date); + } catch (e) { + return dateString; // Retorna o string original se o formato for inválido + } }; - const [itemsPerPage, setItemsPerPage] = useState(5); - const [currentPage, setCurrentPage] = useState(1); + const fetchPacientes = useCallback(async () => { + try { + setLoading(true); + setError(null); + const json = await api.get("/rest/v1/patients"); + const items = Array.isArray(json) + ? json + : Array.isArray(json?.data) + ? json.data + : []; - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); + const mapped: Paciente[] = items.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? "N/A", + cidade: p.city ?? "N/A", + estado: p.state ?? "N/A", + ultimoAtendimento: formatDate(p.created_at), + proximoAtendimento: "N/A", // Necessita de lógica de agendamento real + email: p.email ?? "N/A", + birth_date: p.birth_date ?? "N/A", + cpf: p.cpf ?? "N/A", + blood_type: p.blood_type ?? "N/A", + weight_kg: p.weight_kg ?? 0, + height_m: p.height_m ?? 0, + street: p.street ?? "N/A", + number: p.number ?? "N/A", + complement: p.complement ?? "N/A", + neighborhood: p.neighborhood ?? "N/A", + cep: p.cep ?? "N/A", + })); - const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + setPacientes(mapped); + setCurrentPage(1); // Resetar a página ao carregar novos dados + } catch (e: any) { + console.error("Erro ao carregar pacientes:", e); + setError(e?.message || "Erro ao carregar pacientes"); + } finally { + setLoading(false); + } + }, []); useEffect(() => { - async function fetchPacientes() { - try { - setLoading(true); - setError(null); - const json = await api.get("/rest/v1/patients"); - const items = Array.isArray(json) ? json : (Array.isArray(json?.data) ? json.data : []); - - const mapped = items.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "", - telefone: p.phone_mobile ?? "", - cidade: p.city ?? "", - estado: p.state ?? "", - ultimoAtendimento: formatDate(p.created_at) ?? "", - proximoAtendimento: "", - email: p.email ?? "", - birth_date: p.birth_date ?? "", - cpf: p.cpf ?? "", - blood_type: p.blood_type ?? "", - weight_kg: p.weight_kg ?? 0, - height_m: p.height_m ?? 0, - street: p.street ?? "", - number: p.number ?? "", - complement: p.complement ?? "", - neighborhood: p.neighborhood ?? "", - cep: p.cep ?? "", - })); - - setPacientes(mapped); - } catch (e: any) { - setError(e?.message || "Erro ao carregar pacientes"); - } finally { - setLoading(false); - } - } fetchPacientes(); - }, []); + }, [fetchPacientes]); return ( -
-
-

Pacientes

-

Lista de pacientes vinculados

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

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 */} +
+ + + + +
-
+ +
- +
- - - - - - - + + + + + + + {loading ? ( - ) : error ? ( - + ) : pacientes.length === 0 ? ( @@ -146,17 +249,32 @@ export default function PacientesPage() { ) : ( currentItems.map((p) => ( - - - - - - - - + + + + + + +
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAçõesNome + Telefone + + Cidade + + Estado + + Último atendimento + + Próximo atendimento + Ações
+ + Carregando pacientes...
{`Erro: ${error}`}{`Erro: ${error}`}
{p.nome}{p.telefone}{p.cidade}{p.estado}{p.ultimoAtendimento}{p.proximoAtendimento} +
{p.nome} + {p.telefone} + + {p.cidade} + + {p.estado} + + {p.ultimoAtendimento} + + {p.proximoAtendimento} + - + handleOpenModal(p)}> @@ -164,7 +282,7 @@ export default function PacientesPage() { Ver detalhes - + Laudos @@ -175,11 +293,14 @@ export default function PacientesPage() { { - const newPacientes = pacientes.filter((pac) => pac.id !== p.id) - setPacientes(newPacientes) - alert(`Paciente ID: ${p.id} excluído`) + // Simulação de exclusão (A exclusão real deve ser feita via API) + 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"> + className="text-red-600 focus:bg-red-50 focus:text-red-600" + > Excluir @@ -192,19 +313,51 @@ export default function PacientesPage() {
-
- {Array.from({ length: Math.ceil(pacientes.length / itemsPerPage) }, (_, i) => ( + + {/* Paginação ATUALIZADA */} + {totalPages > 1 && ( +
+ + {/* Botão Anterior */} - ))} -
+ + {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} + + {/* Botão Próximo */} + + +
+ )} + {/* Fim da Paginação ATUALIZADA */} +
+ ADICIONADO: useMemo para otimizar a criação da lista de especialidades import React, { useEffect, useState, useCallback, useMemo } from "react" import ManagerLayout from "@/components/manager-layout"; import Link from "next/link" @@ -10,329 +9,441 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Plus, Edit, Trash2, Eye, Calendar, Filter, MoreVertical, Loader2 } from "lucide-react" import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from "@/components/ui/alert-dialog" import { doctorsService } from "services/doctorsApi.mjs"; interface Doctor { - id: number; - full_name: string; - specialty: string; - crm: string; - phone_mobile: string | null; - city: string | null; - state: string | null; - // -> ADICIONADO: Campo 'status' para que o filtro funcione. Sua API precisa retornar este dado. - status?: string; + id: number; + full_name: string; + specialty: string; + crm: string; + phone_mobile: string | null; + city: string | null; + state: string | null; + status?: string; } interface DoctorDetails { - nome: string; - crm: string; - especialidade: string; - contato: { - celular?: string; - telefone1?: string; - } - endereco: { - cidade?: string; - estado?: string; - } - convenio?: string; - vip?: boolean; - status?: string; - ultimo_atendimento?: string; - proximo_atendimento?: string; - error?: string; + nome: string; + crm: string; + especialidade: string; + contato: { + celular?: string; + telefone1?: string; + } + endereco: { + cidade?: string; + estado?: string; + } + convenio?: string; + vip?: boolean; + status?: string; + ultimo_atendimento?: string; + proximo_atendimento?: string; + error?: string; } export default function DoctorsPage() { - const router = useRouter(); + const router = useRouter(); - const [doctors, setDoctors] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [doctorDetails, setDoctorDetails] = useState(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); + const [doctors, setDoctors] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [doctorDetails, setDoctorDetails] = useState(null); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); - // -> PASSO 1: Criar estados para os filtros - const [specialtyFilter, setSpecialtyFilter] = useState("all"); - const [statusFilter, setStatusFilter] = useState("all"); + // --- Estados para Filtros --- + const [specialtyFilter, setSpecialtyFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + + // --- Estados para Paginação (ADICIONADOS) --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + // --------------------------------------------- - const fetchDoctors = useCallback(async () => { - setLoading(true); - setError(null); - try { - const data: Doctor[] = await doctorsService.list(); - // Exemplo: Adicionando um status fake para o filtro funcionar. O ideal é que isso venha da API. - const dataWithStatus = data.map((doc, index) => ({ - ...doc, - status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo" - })); - setDoctors(dataWithStatus || []); - } catch (e: any) { - console.error("Erro ao carregar lista de médicos:", e); - setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); - setDoctors([]); - } finally { - setLoading(false); - } - }, []); + const fetchDoctors = useCallback(async () => { + setLoading(true); + setError(null); + try { + const data: Doctor[] = await doctorsService.list(); + // Exemplo: Adicionando um status fake + const dataWithStatus = data.map((doc, index) => ({ + ...doc, + status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo" + })); + setDoctors(dataWithStatus || []); + // IMPORTANTE: Resetar a página ao carregar novos dados + setCurrentPage(1); + } catch (e: any) { + console.error("Erro ao carregar lista de médicos:", e); + setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); + setDoctors([]); + } finally { + setLoading(false); + } + }, []); - useEffect(() => { - fetchDoctors(); - }, [fetchDoctors]); + useEffect(() => { + fetchDoctors(); + }, [fetchDoctors]); - const openDetailsDialog = async (doctor: Doctor) => { - setDetailsDialogOpen(true); - setDoctorDetails({ - nome: doctor.full_name, - crm: doctor.crm, - especialidade: doctor.specialty, - contato: { celular: doctor.phone_mobile ?? undefined }, - endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, - status: doctor.status || "Ativo", // Usa o status do médico - convenio: "Particular", - vip: false, - ultimo_atendimento: "N/A", - proximo_atendimento: "N/A", + const openDetailsDialog = async (doctor: Doctor) => { + setDetailsDialogOpen(true); + setDoctorDetails({ + nome: doctor.full_name, + crm: doctor.crm, + especialidade: doctor.specialty, + contato: { celular: doctor.phone_mobile ?? undefined }, + endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, + status: doctor.status || "Ativo", + convenio: "Particular", + vip: false, + ultimo_atendimento: "N/A", + proximo_atendimento: "N/A", + }); + }; + + + const handleDelete = async () => { + if (doctorToDeleteId === null) return; + setLoading(true); + try { + await doctorsService.delete(doctorToDeleteId); + setDeleteDialogOpen(false); + setDoctorToDeleteId(null); + await fetchDoctors(); + } catch (e) { + console.error("Erro ao excluir:", e); + alert("Erro ao excluir médico."); + } finally { + setLoading(false); + } + }; + + const openDeleteDialog = (doctorId: number) => { + setDoctorToDeleteId(doctorId); + setDeleteDialogOpen(true); + }; + + + const handleEdit = (doctorId: number) => { + router.push(`/manager/home/${doctorId}/editar`); + }; + + // Gera a lista de especialidades dinamicamente + const uniqueSpecialties = useMemo(() => { + const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean); + return [...new Set(specialties)]; + }, [doctors]); + + // Lógica de filtragem + const filteredDoctors = doctors.filter(doctor => { + const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; + const statusMatch = statusFilter === "all" || doctor.status === statusFilter; + return specialtyMatch && statusMatch; }); - }; + + // --- Lógica de Paginação (ADICIONADA) --- + // 1. Definição do total de páginas com base nos itens FILTRADOS + const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); + + // 2. Cálculo dos itens a serem exibidos na página atual (dos itens FILTRADOS) + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); + + // 3. Função para mudar de página + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + // 4. Funções para Navegação + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; + + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; + + // 5. Lógica para gerar os números das páginas visíveis + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); + + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } + + 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 + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); // Resetar para a primeira página + }; + // ---------------------------------------------------- - const handleDelete = async () => { - if (doctorToDeleteId === null) return; - setLoading(true); - try { - await doctorsService.delete(doctorToDeleteId); - setDeleteDialogOpen(false); - setDoctorToDeleteId(null); - await fetchDoctors(); - } catch (e) { - console.error("Erro ao excluir:", e); - alert("Erro ao excluir médico."); - } finally { - setLoading(false); - } - }; - - const openDeleteDialog = (doctorId: number) => { - setDoctorToDeleteId(doctorId); - setDeleteDialogOpen(true); - }; - - - const handleEdit = (doctorId: number) => { - router.push(`/manager/home/${doctorId}/editar`); - }; - - // -> MELHORIA: Gera a lista de especialidades dinamicamente - const uniqueSpecialties = useMemo(() => { - const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean); - return [...new Set(specialties)]; - }, [doctors]); - - // -> PASSO 3: Aplicar a lógica de filtragem - const filteredDoctors = doctors.filter(doctor => { - const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; - const statusMatch = statusFilter === "all" || doctor.status === statusFilter; - return specialtyMatch && statusMatch; - }); - - - return ( - -
-
-
-

Médicos Cadastrados

-

Gerencie todos os profissionais de saúde.

-
-
- - - {/* -> PASSO 2: Conectar os estados aos componentes Select <- */} -
- Especialidades - - Status - - -
- - -
- {loading ? ( -
- - Carregando médicos... + return ( + +
+ + {/* Cabeçalho */} +
+
+

Médicos Cadastrados

+

Gerencie todos os profissionais de saúde.

+
+ + +
- ) : error ? ( -
- {error} -
- // -> Atualizado para usar a lista filtrada - ) : filteredDoctors.length === 0 ? ( -
- {doctors.length === 0 - ? <>Nenhum médico cadastrado. Adicione um novo. - : "Nenhum médico encontrado com os filtros aplicados." - } -
- ) : ( -
- - - - - - - - - - - - - - {/* -> ATUALIZADO para mapear a lista filtrada */} - {filteredDoctors.map((doctor) => ( - - - - - {/* Coluna de Status adicionada para visualização */} - - - - - - ))} - -
NomeCRMEspecialidadeStatusCelularCidade/EstadoAções
{doctor.full_name}{doctor.crm}{doctor.specialty}{doctor.status}{doctor.phone_mobile || "N/A"} - {(doctor.city || doctor.state) ? `${doctor.city || ''}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ''}` : "N/A"} - - - -
Ações
-
- - openDetailsDialog(doctor)}> - - Ver detalhes - - handleEdit(doctor.id)}> - - Editar - - openDeleteDialog(doctor.id)} - > - - Excluir - - -
-
+ + {/* Filtros e Itens por Página (ATUALIZADO) */} +
+ + + {/* Filtro de Especialidade */} + + + {/* Filtro de Status */} + + + {/* Select de Itens por Página (ADICIONADO) */} + + +
- )} -
- {/* ... O resto do seu código (AlertDialogs) permanece o mesmo ... */} - - - - Confirma a exclusão? - - Esta ação é irreversível e excluirá permanentemente o registro deste médico. - - - - Cancelar - - {loading ? : null} - Excluir - - - - - - - - {doctorDetails?.nome} - - {doctorDetails && ( -
-

Informações Principais

-
-
CRM: {doctorDetails.crm}
-
Especialidade: {doctorDetails.especialidade}
-
Celular: {doctorDetails.contato.celular || 'N/A'}
-
Localização: {`${doctorDetails.endereco.cidade || 'N/A'}/${doctorDetails.endereco.estado || 'N/A'}`}
+ {/* Tabela de Médicos */} +
+ {loading ? ( +
+ + Carregando médicos...
- -

Atendimento e Convênio

-
-
Convênio: {doctorDetails.convenio || 'N/A'}
-
VIP: {doctorDetails.vip ? "Sim" : "Não"}
-
Status: {doctorDetails.status || 'N/A'}
-
Último atendimento: {doctorDetails.ultimo_atendimento || 'N/A'}
-
Próximo atendimento: {doctorDetails.proximo_atendimento || 'N/A'}
+ ) : error ? ( +
{error}
+ ) : filteredDoctors.length === 0 ? ( +
+ {doctors.length === 0 + ? <>Nenhum médico cadastrado. Adicione um novo. + : "Nenhum médico encontrado com os filtros aplicados." + } +
+ ) : ( +
+ + + + + + + + + + + + + {/* Mapeia APENAS os itens da página atual (currentItems) */} + {currentItems.map((doctor) => ( + + + + + + + + + ))} + +
NomeCRMEspecialidadeStatusCidade/EstadoAções
{doctor.full_name}{doctor.crm}{doctor.specialty}{doctor.status || "N/A"} + {(doctor.city || doctor.state) + ? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}` + : "N/A"} + +
+ + + + + + + + + Agendar Consulta + + +
+
-
)} - {doctorDetails === null && !loading && ( -
Detalhes não disponíveis.
- )} - - - - Fechar - - - +
+ + {/* Paginação (ADICIONADA) */} + {totalPages > 1 && ( +
+ + {/* Botão Anterior */} + -
- - ); + {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} + + {/* Botão Próximo */} + + +
+ )} + + {/* Dialogs de Exclusão e Detalhes (Sem alterações) */} + + + + Confirma a exclusão? + + Esta ação é irreversível e excluirá permanentemente o registro deste médico. + + + + Cancelar + + {loading ? : null} + Excluir + + + + + + + + + {doctorDetails?.nome} + + {doctorDetails && ( +
+

Informações Principais

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

Atendimento e Convênio

+
+
Convênio: {doctorDetails.convenio || 'N/A'}
+
VIP: {doctorDetails.vip ? "Sim" : "Não"}
+
Status: {doctorDetails.status || 'N/A'}
+
Último atendimento: {doctorDetails.ultimo_atendimento || 'N/A'}
+
Próximo atendimento: {doctorDetails.proximo_atendimento || 'N/A'}
+
+
+ )} + {doctorDetails === null && !loading && ( +
Detalhes não disponíveis.
+ )} +
+
+ + Fechar + +
+
+
+ + ); } \ No newline at end of file diff --git a/app/manager/pacientes/page.tsx b/app/manager/pacientes/page.tsx index 3e77185..b4fd861 100644 --- a/app/manager/pacientes/page.tsx +++ b/app/manager/pacientes/page.tsx @@ -30,34 +30,32 @@ import { import ManagerLayout from "@/components/manager-layout"; import { patientsService } from "@/services/patientsApi.mjs"; -// --- INÍCIO DA MODIFICAÇÃO --- -// PASSO 1: Criar uma função para formatar a data +// 📅 PASSO 1: Criar uma função para formatar a data const formatDate = (dateString: string | null | undefined): string => { - // Se a data não existir, retorna um texto padrão - if (!dateString) { - return "N/A"; + // Se a data não existir, retorna um texto padrão + if (!dateString) { + return "N/A"; + } + + try { + const date = new Date(dateString); + // Verifica se a data é válida após a conversão + if (isNaN(date.getTime())) { + return "Data inválida"; } - try { - const date = new Date(dateString); - // Verifica se a data é válida após a conversão - if (isNaN(date.getTime())) { - return "Data inválida"; - } + const day = String(date.getDate()).padStart(2, '0'); + const month = String(date.getMonth() + 1).padStart(2, '0'); // Mês é base 0, então +1 + const year = date.getFullYear(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); // Mês é base 0, então +1 - const year = date.getFullYear(); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - - return `${day}/${month}/${year} ${hours}:${minutes}`; - } catch (error) { - // Se houver qualquer erro na conversão, retorna um texto de erro - return "Data inválida"; - } + return `${day}/${month}/${year} ${hours}:${minutes}`; + } catch (error) { + // Se houver qualquer erro na conversão, retorna um texto de erro + return "Data inválida"; + } }; -// --- FIM DA MODIFICAÇÃO --- export default function PacientesPage() { @@ -257,86 +255,84 @@ export default function PacientesPage() {
-
-
- {error ? ( -
{`Erro ao carregar pacientes: ${error}`}
- ) : ( - - - - - - - - - - - - - - {filteredPatients.length === 0 ? ( - - - - ) : ( - filteredPatients.map((patient) => ( - - - - - - {/* --- INÍCIO DA MODIFICAÇÃO --- */} - {/* PASSO 2: Aplicar a formatação de data na tabela */} - - - {/* --- FIM DA MODIFICAÇÃO --- */} - - - )) - )} - -
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAções
- {patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
-
-
- {patient.nome?.charAt(0) || "?"} -
- {patient.nome} -
-
{patient.telefone}{patient.cidade}{patient.estado}{formatDate(patient.ultimoAtendimento)}{formatDate(patient.proximoAtendimento)} - - -
Ações
-
- - openDetailsDialog(String(patient.id))}> - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(String(patient.id))}> - - Excluir - - -
-
- )} -
- {isFetching &&
Carregando mais pacientes...
} -
-
+
+
+ {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : ( + + + + + + + + + + + + + + {filteredPatients.length === 0 ? ( + + + + ) : ( + filteredPatients.map((patient) => ( + + + + + + {/* 📅 PASSO 2: Aplicar a formatação de data na tabela */} + + + + + )) + )} + +
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAções
+ {patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} +
+
+
+ {patient.nome?.charAt(0) || "?"} +
+ {patient.nome} +
+
{patient.telefone}{patient.cidade}{patient.estado}{formatDate(patient.ultimoAtendimento)}{formatDate(patient.proximoAtendimento)} + + +
Ações
+
+ + openDetailsDialog(String(patient.id))}> + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + openDeleteDialog(String(patient.id))}> + + Excluir + + +
+
+ )} +
+ {isFetching &&
Carregando mais pacientes...
} +
+
@@ -361,87 +357,85 @@ export default function PacientesPage() { - {/* Modal de detalhes do paciente */} - - - - Detalhes do Paciente - - {patientDetails === null ? ( -
Carregando...
- ) : patientDetails?.error ? ( -
{patientDetails.error}
- ) : ( -
-

- Nome: {patientDetails.full_name} -

-

- CPF: {patientDetails.cpf} -

-

- Email: {patientDetails.email} -

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

- {/* --- INÍCIO DA MODIFICAÇÃO --- */} - {/* PASSO 3: Aplicar a formatação de data no modal */} -

- Criado em: {formatDate(patientDetails.created_at)} -

-

- Atualizado em: {formatDate(patientDetails.updated_at)} -

- {/* --- FIM DA MODIFICAÇÃO --- */} -

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

-
- )} -
-
- - Fechar - -
-
-
- - ); + {/* Modal de detalhes do paciente */} + + + + Detalhes do Paciente + + {patientDetails === null ? ( +
Carregando...
+ ) : patientDetails?.error ? ( +
{patientDetails.error}
+ ) : ( +
+

+ Nome: {patientDetails.full_name} +

+

+ CPF: {patientDetails.cpf} +

+

+ Email: {patientDetails.email} +

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+ {/* 📅 PASSO 3: Aplicar a formatação de data no modal */} +

+ Criado em: {formatDate(patientDetails.created_at)} +

+

+ Atualizado em: {formatDate(patientDetails.updated_at)} +

+

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

+
+ )} +
+
+ + Fechar + +
+
+
+
+ ); } \ No newline at end of file diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index 660db2a..ece5c97 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -6,239 +6,412 @@ import ManagerLayout from "@/components/manager-layout"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Plus, Eye, Filter, Loader2 } from "lucide-react"; import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { api, login } from "services/api.mjs"; import { usersService } from "services/usersApi.mjs"; interface FlatUser { - id: string; - user_id: string; - full_name?: string; - email: string; - phone?: string | null; - role: string; + id: string; + user_id: string; + full_name?: string; + email: string; + phone?: string | null; + role: string; } interface UserInfoResponse { - user: any; - profile: any; - roles: string[]; - permissions: Record; + user: any; + profile: any; + roles: string[]; + permissions: Record; } export default function UsersPage() { - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [userDetails, setUserDetails] = useState(null); - const [selectedRole, setSelectedRole] = useState(""); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [userDetails, setUserDetails] = useState( + null + ); + // Ajuste 1: Definir 'all' como valor inicial para garantir que todos os usuários sejam exibidos por padrão. + const [selectedRole, setSelectedRole] = useState("all"); - const fetchUsers = useCallback(async () => { - setLoading(true); - setError(null); - try { - // 1) pega roles - const rolesData: any[] = await usersService.list_roles(); - // Garante que rolesData é array - const rolesArray = Array.isArray(rolesData) ? rolesData : []; + // --- Lógica de Paginação INÍCIO --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); - // 2) pega todos os profiles de uma vez (para evitar muitos requests) - const profilesData: any[] = await api.get(`/rest/v1/profiles?select=id,full_name,email,phone`); - const profilesById = new Map(); - if (Array.isArray(profilesData)) { - for (const p of profilesData) { - if (p?.id) profilesById.set(p.id, p); - } - } - - // 3) mapear roles -> flat users, usando ID específico de cada item - const mapped: FlatUser[] = rolesArray.map((roleItem) => { - const uid = roleItem.user_id; - const profile = profilesById.get(uid); - return { - id: uid, - user_id: uid, - full_name: profile?.full_name ?? "—", - email: profile?.email ?? "—", - phone: profile?.phone ?? "—", - role: roleItem.role ?? "—", - }; - }); - - setUsers(mapped); - console.log("[fetchUsers] mapped count:", mapped.length); - } catch (err: any) { - console.error("Erro ao buscar usuários:", err); - setError("Não foi possível carregar os usuários. Veja console."); - setUsers([]); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - const init = async () => { - try { - await login(); // garante token - } catch (e) { - console.warn("login falhou no init:", e); - } - await fetchUsers(); + // Lógica para mudar itens por página, resetando para a página 1 + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); // Resetar para a primeira página }; - init(); - }, [fetchUsers]); + // --- Lógica de Paginação FIM --- - const openDetailsDialog = async (flatUser: FlatUser) => { - setDetailsDialogOpen(true); - setUserDetails(null); + const fetchUsers = useCallback(async () => { + setLoading(true); + setError(null); + try { + const rolesData: any[] = await usersService.list_roles(); + const rolesArray = Array.isArray(rolesData) ? rolesData : []; - try { - console.log("[openDetailsDialog] user_id:", flatUser.user_id); - const data = await usersService.full_data(flatUser.user_id); - console.log("[openDetailsDialog] full_data returned:", data); - setUserDetails(data); - } catch (err: any) { - console.error("Erro ao carregar detalhes:", err); - // fallback com dados já conhecidos - setUserDetails({ - user: { id: flatUser.user_id, email: flatUser.email }, - profile: { full_name: flatUser.full_name, phone: flatUser.phone }, - roles: [flatUser.role], - permissions: {}, - }); - } - }; + const profilesData: any[] = await api.get( + `/rest/v1/profiles?select=id,full_name,email,phone` + ); - const filteredUsers = - selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users; + const profilesById = new Map(); + if (Array.isArray(profilesData)) { + for (const p of profilesData) { + if (p?.id) profilesById.set(p.id, p); + } + } - return ( - -
-
-
-

Usuários

-

Gerencie usuários.

-
- - - -
+ const mapped: FlatUser[] = rolesArray.map((roleItem) => { + const uid = roleItem.user_id; + const profile = profilesById.get(uid); + return { + id: uid, + user_id: uid, + full_name: profile?.full_name ?? "—", + email: profile?.email ?? "—", + phone: profile?.phone ?? "—", + role: roleItem.role ?? "—", + }; + }); -
- - -
+ setUsers(mapped); + setCurrentPage(1); // Resetar a página após carregar + console.log("[fetchUsers] mapped count:", mapped.length); + } catch (err: any) { + console.error("Erro ao buscar usuários:", err); + setError("Não foi possível carregar os usuários. Veja console."); + setUsers([]); + } finally { + setLoading(false); + } + }, []); -
- {loading ? ( -
- - Carregando usuários... -
- ) : error ? ( -
{error}
- ) : filteredUsers.length === 0 ? ( -
- Nenhum usuário encontrado. -
- ) : ( -
- - - - - - - - - - - - - {filteredUsers.map((u) => ( - - - - - - - - - ))} - -
IDNomeE-mailTelefoneCargoAções
{u.id}{u.full_name}{u.email}{u.phone}{u.role} - -
-
- )} -
+ useEffect(() => { + const init = async () => { + try { + await login(); + } catch (e) { + console.warn("login falhou no init:", e); + } + await fetchUsers(); + }; + init(); + }, [fetchUsers]); - - - - {userDetails?.profile?.full_name || "Detalhes do Usuário"} - - {!userDetails ? ( -
- - Buscando dados completos... -
- ) : ( -
-
ID: {userDetails.user.id}
-
E-mail: {userDetails.user.email}
-
Nome completo: {userDetails.profile.full_name}
-
Telefone: {userDetails.profile.phone}
-
Roles: {userDetails.roles?.join(", ")}
+ const openDetailsDialog = async (flatUser: FlatUser) => { + setDetailsDialogOpen(true); + setUserDetails(null); + + try { + console.log("[openDetailsDialog] user_id:", flatUser.user_id); + const data = await usersService.full_data(flatUser.user_id); + console.log("[openDetailsDialog] full_data returned:", data); + setUserDetails(data); + } catch (err: any) { + console.error("Erro ao carregar detalhes:", err); + setUserDetails({ + user: { id: flatUser.user_id, email: flatUser.email }, + profile: { full_name: flatUser.full_name, phone: flatUser.phone }, + roles: [flatUser.role], + permissions: {}, + }); + } + }; + + // 1. Filtragem + const filteredUsers = + selectedRole && selectedRole !== "all" + ? users.filter((u) => u.role === selectedRole) + : users; + + // 2. Paginação (aplicada sobre a lista filtrada) + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredUsers.slice(indexOfFirstItem, indexOfLastItem); + + // Função para mudar de página + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); + + const totalPages = Math.ceil(filteredUsers.length / itemsPerPage); + + // --- Funções e Lógica de Navegação ADICIONADAS --- + const goToPrevPage = () => { + setCurrentPage((prev) => Math.max(1, prev - 1)); + }; + + const goToNextPage = () => { + setCurrentPage((prev) => Math.min(totalPages, prev + 1)); + }; + + // Lógica para gerar os números das páginas visíveis + const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { + const pages: number[] = []; + const maxVisiblePages = 5; // Número máximo de botões de página a serem exibidos (ex: 2, 3, 4, 5, 6) + const halfRange = Math.floor(maxVisiblePages / 2); + let startPage = Math.max(1, currentPage - halfRange); + let endPage = Math.min(totalPages, currentPage + halfRange); + + // Ajusta para manter o número fixo de botões quando nos limites + if (endPage - startPage + 1 < maxVisiblePages) { + if (endPage === totalPages) { + startPage = Math.max(1, totalPages - maxVisiblePages + 1); + } + if (startPage === 1) { + endPage = Math.min(totalPages, maxVisiblePages); + } + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + return pages; + }; + + const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); + // --- Fim das Funções e Lógica de Navegação ADICIONADAS --- + + + return ( + +
+ + {/* Header */} +
- Permissões: -
    - {Object.entries(userDetails.permissions || {}).map(([k,v]) =>
  • {k}: {v ? "Sim" : "Não"}
  • )} -
+

Usuários

+

Gerencie usuários.

-
- )} - - - - Fechar - - - -
-
- ); -} + + + +
+ + {/* Filtro e Itens por Página */} +
+ + + {/* Select de Filtro por Papel - Ajustado para resetar a página */} + + + {/* Select de Itens por Página */} + +
+ {/* Fim do Filtro e Itens por Página */} + + {/* Tabela */} +
+ {loading ? ( +
+ + Carregando usuários... +
+ ) : error ? ( +
{error}
+ ) : filteredUsers.length === 0 ? ( +
+ Nenhum usuário encontrado com os filtros aplicados. +
+ ) : ( + <> + + + + + + + + + + + + + {/* Usando currentItems para a paginação */} + {currentItems.map((u) => ( + + + + + + + + + ))} + +
IDNomeE-mailTelefoneCargoAções
+ {u.id} + + {u.full_name} + + {u.email} + + {u.phone} + + {u.role} + + +
+ + {/* Paginação ATUALIZADA */} + {totalPages > 1 && ( +
+ + {/* Botão Anterior */} + + + {/* Números das Páginas */} + {visiblePageNumbers.map((number) => ( + + ))} + + {/* Botão Próximo */} + + +
+ )} + {/* Fim da Paginação ATUALIZADA */} + + )} +
+ + {/* Modal de Detalhes */} + + + + + {userDetails?.profile?.full_name || "Detalhes do Usuário"} + + + {!userDetails ? ( +
+ + Buscando dados completos... +
+ ) : ( +
+
+ ID: {userDetails.user.id} +
+
+ E-mail: {userDetails.user.email} +
+
+ Nome completo:{" "} + {userDetails.profile.full_name} +
+
+ Telefone: {userDetails.profile.phone} +
+
+ Roles:{" "} + {userDetails.roles?.join(", ")} +
+ {/* Melhoria na visualização das permissões no modal */} +
+ Permissões: +
    + {Object.entries( + userDetails.permissions || {} + ).map(([k, v]) => ( +
  • + {k}: {v ? "Sim" : "Não"} +
  • + ))} +
+
+
+ )} +
+
+ + Fechar + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/secretary/pacientes/page.tsx b/app/secretary/pacientes/page.tsx index 624e2aa..b61716a 100644 --- a/app/secretary/pacientes/page.tsx +++ b/app/secretary/pacientes/page.tsx @@ -1,404 +1,466 @@ +// app/secretary/pacientes/page.tsx "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Plus, Edit, Trash2, Eye, Calendar, Filter } 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 { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import SecretaryLayout from "@/components/secretary-layout"; import { patientsService } from "@/services/patientsApi.mjs"; -// --- INÍCIO DA CORREÇÃO --- -interface Patient { - id: string; - nome: string; - telefone: string; - cidade: string; - estado: string; - ultimoAtendimento: string; - proximoAtendimento: string; - vip: boolean; - convenio: string; - status?: string; - // Propriedades detalhadas para o modal - full_name?: string; - cpf?: string; - email?: string; - phone_mobile?: string; - phone1?: string; - phone2?: string; - social_name?: string; - sex?: string; - blood_type?: string; - weight_kg?: number; - height_m?: number; - bmi?: number; - street?: string; - neighborhood?: string; - city?: string; // <-- Adicionado - state?: string; // <-- Adicionado - cep?: string; - created_at?: string; - updated_at?: string; -} -// --- FIM DA CORREÇÃO --- - -// Função para formatar a data -const formatDate = (dateString: string | null | undefined): string => { - if (!dateString) { - return "N/A"; - } - try { - const date = new Date(dateString); - if (isNaN(date.getTime())) { - return "Data inválida"; - } - const day = String(date.getDate()).padStart(2, '0'); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const year = date.getFullYear(); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - return `${day}/${month}/${year} ${hours}:${minutes}`; - } catch (error) { - return "Data inválida"; - } -}; +// Defina o tamanho da página. +const PAGE_SIZE = 5; export default function PacientesPage() { - const [searchTerm, setSearchTerm] = useState(""); - const [convenioFilter, setConvenioFilter] = useState("all"); - const [vipFilter, setVipFilter] = useState("all"); - const [patients, setPatients] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [page, setPage] = useState(1); - const [hasNext, setHasNext] = useState(true); - const [isFetching, setIsFetching] = useState(false); - const observerRef = useRef(null); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [patientToDelete, setPatientToDelete] = useState(null); - const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); - const [patientDetails, setPatientDetails] = useState(null); + // --- ESTADOS DE DADOS E GERAL --- + const [searchTerm, setSearchTerm] = useState(""); + const [convenioFilter, setConvenioFilter] = useState("all"); + const [vipFilter, setVipFilter] = useState("all"); + + // Lista completa, carregada da API uma única vez + const [allPatients, setAllPatients] = useState([]); + // Lista após a aplicação dos filtros (base para a paginação) + const [filteredPatients, setFilteredPatients] = useState([]); - const openDetailsDialog = async (patientId: string) => { - setDetailsDialogOpen(true); - setPatientDetails(null); - try { - const res = await patientsService.getById(patientId); - setPatientDetails(res[0]); - } catch (e: any) { - setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); - } - }; + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // --- ESTADOS DE PAGINAÇÃO --- + const [page, setPage] = useState(1); + + // CÁLCULO DA PAGINAÇÃO + const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); + const startIndex = (page - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + // Pacientes a serem exibidos na tabela (aplicando a paginação) + const currentPatients = filteredPatients.slice(startIndex, endIndex); - const fetchPacientes = useCallback( - async (pageToFetch: number) => { - if (isFetching || !hasNext) return; - setIsFetching(true); - setError(null); - try { - const res = await patientsService.list(); - const mapped: Patient[] = res.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "", - telefone: p.phone_mobile ?? p.phone1 ?? "", - cidade: p.city ?? "", - estado: p.state ?? "", - ultimoAtendimento: p.last_visit_at ?? "", - proximoAtendimento: p.next_appointment_at ?? "", - vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "", - status: p.status ?? undefined, - })); + // --- ESTADOS DE DIALOGS --- + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); + const [patientDetails, setPatientDetails] = useState(null); + + // --- FUNÇÕES DE LÓGICA --- - setPatients((prev) => { - const all = [...prev, ...mapped]; - const unique = Array.from(new Map(all.map((p) => [p.id, p])).values()); - return unique; + // 1. Função para carregar TODOS os pacientes da API + const fetchAllPacientes = useCallback( + async () => { + setLoading(true); + setError(null); + try { + // Como o backend retorna um array, chamamos sem paginação + const res = await patientsService.list(); + + const mapped = res.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "—", + telefone: p.phone_mobile ?? p.phone1 ?? "—", + cidade: p.city ?? "—", + estado: p.state ?? "—", + // Formate as datas se necessário, aqui usamos como string + ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—", + proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—", + vip: Boolean(p.vip ?? false), + convenio: p.convenio ?? "Particular", // Define um valor padrão + status: p.status ?? undefined, + })); + + setAllPatients(mapped); + } catch (e: any) { + console.error(e); + setError(e?.message || "Erro ao buscar pacientes"); + } finally { + setLoading(false); + } + }, + [] + ); + + // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) + useEffect(() => { + const filtered = allPatients.filter((patient) => { + // Filtro por termo de busca (Nome ou Telefone) + const matchesSearch = + patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || + patient.telefone?.includes(searchTerm); + + // Filtro por Convênio + const matchesConvenio = + convenioFilter === "all" || + patient.convenio === convenioFilter; + + // Filtro por VIP + const matchesVip = + vipFilter === "all" || + (vipFilter === "vip" && patient.vip) || + (vipFilter === "regular" && !patient.vip); + + return matchesSearch && matchesConvenio && matchesVip; }); + + setFilteredPatients(filtered); + // Garante que a página atual seja válida após a filtragem + setPage(1); + }, [allPatients, searchTerm, convenioFilter, vipFilter]); - if (mapped.length === 0) { - setHasNext(false); - } else { - setPage((prev) => prev + 1); + // 3. Efeito inicial para buscar os pacientes + useEffect(() => { + fetchAllPacientes(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + + // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- + + const openDetailsDialog = async (patientId: string) => { + setDetailsDialogOpen(true); + setPatientDetails(null); + try { + const res = await patientsService.getById(patientId); + setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item + } catch (e: any) { + setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); } - - } catch (e: any) { - setError(e?.message || "Erro ao buscar pacientes"); - } finally { - setIsFetching(false); - } - }, - [isFetching, hasNext] - ); - - useEffect(() => { - fetchPacientes(1); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (!observerRef.current || !hasNext) return; - const observer = new window.IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isFetching && hasNext) { - fetchPacientes(page); - } - }); - observer.observe(observerRef.current); - return () => { - if (observerRef.current) { - observer.unobserve(observerRef.current); - } }; - }, [fetchPacientes, page, hasNext, isFetching]); - const handleDeletePatient = async (patientId: string) => { - try { - await patientsService.delete(patientId); - setPatients((prev) => prev.filter((p) => p.id !== patientId)); - } catch (e: any) { - setError(e?.message || "Erro ao deletar paciente"); - alert("Erro ao deletar paciente."); - } - setDeleteDialogOpen(false); - setPatientToDelete(null); - }; + const handleDeletePatient = async (patientId: string) => { + try { + await patientsService.delete(patientId); + // Atualiza a lista completa para refletir a exclusão + setAllPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId))); + } catch (e: any) { + alert(`Erro ao deletar paciente: ${e?.message || 'Erro desconhecido'}`); + } + setDeleteDialogOpen(false); + setPatientToDelete(null); + }; - const openDeleteDialog = (patientId: string) => { - setPatientToDelete(patientId); - setDeleteDialogOpen(true); - }; + const openDeleteDialog = (patientId: string) => { + setPatientToDelete(patientId); + setDeleteDialogOpen(true); + }; - const filteredPatients = patients.filter((patient) => { - const matchesSearch = - patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || - patient.telefone?.includes(searchTerm); - const matchesConvenio = - convenioFilter === "all" || (patient.convenio ?? "") === convenioFilter; - const matchesVip = - vipFilter === "all" || - (vipFilter === "vip" && patient.vip) || - (vipFilter === "regular" && !patient.vip); + return ( + +
+ {/* Header (Responsividade OK) */} +
+
+

Pacientes

+

Gerencie as informações de seus pacientes

+
+
+ + + +
+
- return matchesSearch && matchesConvenio && matchesVip; - }); + {/* Bloco de Filtros (Responsividade APLICADA) */} +
+ + + {/* Busca - Ocupa 100% no mobile, depois cresce */} + setSearchTerm(e.target.value)} + className="w-full sm:flex-grow sm:min-w-[150px] p-2 border rounded-md text-sm" + /> - return ( - -
-
-
-

Pacientes

-

Gerencie as informações de seus pacientes

-
-
- - - -
-
+ {/* Convênio - Ocupa metade da linha no mobile */} +
+ Convênio + +
-
-
- Convênio - -
-
- VIP - -
-
- Aniversariantes - -
- -
+ {/* VIP - Ocupa a outra metade da linha no mobile */} +
+ VIP + +
+ + {/* Aniversariantes - Vai para a linha de baixo no mobile, ocupando 100% */} + +
-
-
- {error &&
{`Erro ao carregar pacientes: ${error}`}
} - - - - - - - - - - - - - - {filteredPatients.length === 0 && !isFetching ? ( - - - - ) : ( - filteredPatients.map((patient) => ( - - - - - - - - - - )) - )} - -
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAções
- {patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} -
-
-
- {patient.nome?.charAt(0) || "?"} -
- {patient.nome} + {/* Tabela (Responsividade APLICADA) */} +
+
+ {error ? ( +
{`Erro ao carregar pacientes: ${error}`}
+ ) : loading ? ( +
+ Carregando pacientes... +
+ ) : ( + // min-w ajustado para responsividade + + + + + {/* Coluna oculta em telas muito pequenas */} + + {/* Coluna oculta em telas pequenas e muito pequenas */} + + {/* Coluna oculta em telas muito pequenas */} + + {/* Colunas ocultas em telas médias, pequenas e muito pequenas */} + + + + + + + {currentPatients.length === 0 ? ( + + + + ) : ( + currentPatients.map((patient) => ( + + + {/* Aplicação das classes de visibilidade */} + + + + + + + + + )) + )} + +
NomeTelefoneCidade / EstadoConvênioÚltimo atendimentoPróximo atendimentoAções
+ {allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} +
+
+
+ {patient.nome?.charAt(0) || "?"} +
+ + {patient.nome} + {patient.vip && ( + VIP + )} + +
+
{patient.telefone}{`${patient.cidade} / ${patient.estado}`}{patient.convenio}{patient.ultimoAtendimento}{patient.proximoAtendimento} + + +
Ações
+
+ + openDetailsDialog(String(patient.id))}> + + Ver detalhes + + + + + + Editar + + + + + + Marcar consulta + + openDeleteDialog(String(patient.id))}> + + Excluir + + +
+
+ )} +
+ + {/* Paginação */} + {totalPages > 1 && !loading && ( +
+ {/* Adicionado contador de página para melhor informação no mobile */} + + {/* Botões de Navegação */} +
+ {/* Botão Anterior */} + + + {/* Renderização dos botões de número de página (Limitando a 5) */} + {Array.from({ length: totalPages }, (_, index) => index + 1) + // Limita a exibição dos botões para 5 (janela de visualização) + .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) + .map((pageNumber) => ( + + ))} + + {/* Botão Próximo */} + +
-
{patient.telefone}{patient.cidade}{patient.estado}{formatDate(patient.ultimoAtendimento)}{formatDate(patient.proximoAtendimento)} - - - - - - openDetailsDialog(patient.id)}> - - Ver detalhes - - - - - Editar - - - - - Marcar consulta - - openDeleteDialog(patient.id)}> - - Excluir - - - -
-
- {isFetching &&
Carregando mais pacientes...
} -
-
+ )} +
+ + {/* AlertDialogs (Permanecem os mesmos) */} + + {/* ... (AlertDialog de Exclusão) ... */} + + + Confirmar exclusão + Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. + + + Cancelar + patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + + - - - - Confirmar exclusão - Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. - - - Cancelar - patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> - Excluir - - - - - - - - - Detalhes do Paciente - {patientDetails === null ? ( -
Carregando...
- ) : 'error' in patientDetails ? ( -
{patientDetails.error}
- ) : ( - -
-

Nome: {patientDetails.full_name}

-

CPF: {patientDetails.cpf || "-"}

-

Email: {patientDetails.email || "-"}

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

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

-

Criado em: {formatDate(patientDetails.created_at)}

-

Atualizado em: {formatDate(patientDetails.updated_at)}

-

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

-
-
- )} -
- - Fechar - -
-
-
- - ); + + {/* ... (AlertDialog de Detalhes) ... */} + + + Detalhes do Paciente + + {patientDetails === null ? ( +
+ + Carregando... +
+ ) : patientDetails?.error ? ( +
{patientDetails.error}
+ ) : ( +
+
+
+

Nome Completo

+

{patientDetails.full_name}

+
+
+

Email

+

{patientDetails.email}

+
+
+

Telefone

+

{patientDetails.phone_mobile}

+
+
+

Data de Nascimento

+

{patientDetails.birth_date}

+
+
+

CPF

+

{patientDetails.cpf}

+
+
+

Tipo Sanguíneo

+

{patientDetails.blood_type}

+
+
+

Peso (kg)

+

{patientDetails.weight_kg}

+
+
+

Altura (m)

+

{patientDetails.height_m}

+
+
+
+

Endereço

+
+
+

Rua

+

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

+
+
+

Complemento

+

{patientDetails.complement}

+
+
+

Bairro

+

{patientDetails.neighborhood}

+
+
+

Cidade

+

{patientDetails.cidade}

+
+
+

Estado

+

{patientDetails.estado}

+
+
+

CEP

+

{patientDetails.cep}

+
+
+
+
+ )} +
+
+ + Fechar + +
+
+
+
+ ); } \ No newline at end of file