diff --git a/app/manager/home/page.tsx b/app/manager/home/page.tsx index 71ac516..7ce5783 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -7,7 +7,7 @@ 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 { Plus, Edit, Trash2, Eye, Calendar, Filter, MoreVertical, Loader2 } from "lucide-react" +import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react" import { AlertDialog, AlertDialogAction, @@ -69,25 +69,21 @@ export default function DoctorsPage() { const [specialtyFilter, setSpecialtyFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); - // --- Estados para Paginação (ADICIONADOS) --- + // --- Estados para Paginação --- 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 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); + 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."); @@ -141,37 +137,23 @@ export default function DoctorsPage() { 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)); }; @@ -179,8 +161,7 @@ export default function DoctorsPage() { 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; @@ -196,254 +177,265 @@ export default function DoctorsPage() { 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 + setCurrentPage(1); }; - // ---------------------------------------------------- return ( -
- - {/* Cabeçalho */} -
-
-

Médicos Cadastrados

-

Gerencie todos os profissionais de saúde.

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

Médicos Cadastrados

+

Gerencie todos os profissionais de saúde.

+
+ + +
- - - -
+
- {/* Filtros e Itens por Página (ATUALIZADO) */} -
- - - {/* Filtro de Especialidade */} - - - {/* Filtro de Status */} - - - {/* Select de Itens por Página (ADICIONADO) */} - - - -
- - - {/* 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." - } -
- ) : ( -
- - - - - - - - - - - - - {/* 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"} - -
- - - + {/* 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}{doctor.specialty}{doctor.status || "N/A"} + {(doctor.city || doctor.state) + ? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}` + : "N/A"} + + {/* ===== INÍCIO DA ALTERAÇÃO ===== */} - +
Ações
- Agendar Consulta + openDetailsDialog(doctor)}> + + Ver detalhes + + + + + Editar + + + + + Marcar consulta + + openDeleteDialog(doctor.id)}> + + Excluir +
- -
-
- )} -
- - {/* Paginação (ADICIONADA) */} - {totalPages > 1 && ( -
- - {/* Botão Anterior */} - + {/* ===== FIM DA ALTERAÇÃO ===== */} +
+
+ )} +
- {/* Números das Páginas */} - {visiblePageNumbers.map((number) => ( + {/* Paginação */} + {totalPages > 1 && ( +
+ + {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 - - - - + {/* 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 + + + + - - - - {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'}`}
+ + + + {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'}
+
- -

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 - - - -
+ )} + {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 b4fd861..abe4841 100644 --- a/app/manager/pacientes/page.tsx +++ b/app/manager/pacientes/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; import { @@ -16,7 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Plus, Edit, Trash2, Eye, Calendar, Filter } from "lucide-react"; +import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, @@ -30,29 +30,22 @@ import { import ManagerLayout from "@/components/manager-layout"; import { patientsService } from "@/services/patientsApi.mjs"; -// 📅 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"; } - 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 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) { - // Se houver qualquer erro na conversão, retorna um texto de erro return "Data inválida"; } }; @@ -63,16 +56,19 @@ export default function PacientesPage() { const [convenioFilter, setConvenioFilter] = useState("all"); const [vipFilter, setVipFilter] = useState("all"); const [patients, setPatients] = useState([]); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); // Alterado para true const [error, setError] = useState(null); - const [page, setPage] = useState(1); - const [hasNext, setHasNext] = useState(true); - const [isFetching, setIsFetching] = useState(false); - const observerRef = useRef(null); + + // --- Estados de Paginação (ADICIONADOS) --- + const [itemsPerPage, setItemsPerPage] = useState(10); + const [currentPage, setCurrentPage] = useState(1); + // --------------------------------------------- + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [patientToDelete, setPatientToDelete] = useState(null); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [patientDetails, setPatientDetails] = useState(null); + const openDetailsDialog = async (patientId: string) => { setDetailsDialogOpen(true); setPatientDetails(null); @@ -84,75 +80,43 @@ export default function PacientesPage() { } }; - const fetchPacientes = useCallback( - async (pageToFetch: number) => { - if (isFetching || !hasNext) return; - setIsFetching(true); - setError(null); - try { - const res = await patientsService.list(); - const mapped = res.map((p: any) => ({ - id: String(p.id ?? ""), - nome: p.full_name ?? "", - telefone: p.phone_mobile ?? p.phone1 ?? "", - cidade: p.city ?? "", - estado: p.state ?? "", - ultimoAtendimento: p.last_visit_at ?? "", - proximoAtendimento: p.next_appointment_at ?? "", - vip: Boolean(p.vip ?? false), - convenio: p.convenio ?? "", // se não existir, fica vazio - status: p.status ?? undefined, - })); - - setPatients((prev) => { - const all = [...prev, ...mapped]; - const unique = Array.from( - new Map(all.map((p) => [p.id, p])).values() - ); - return unique; - }); - - if (!mapped.id) setHasNext(false); // parar carregamento - else setPage((prev) => prev + 1); - } catch (e: any) { - setError(e?.message || "Erro ao buscar pacientes"); - } finally { - setIsFetching(false); - } - }, - [isFetching, hasNext] - ); - - useEffect(() => { - fetchPacientes(page); - // eslint-disable-next-line react-hooks/exhaustive-deps + // --- LÓGICA DE BUSCA DE DADOS (ATUALIZADA) --- + const fetchPacientes = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await patientsService.list(); + const mapped = res.map((p: any) => ({ + id: String(p.id ?? ""), + nome: p.full_name ?? "", + telefone: p.phone_mobile ?? p.phone1 ?? "", + cidade: p.city ?? "", + estado: p.state ?? "", + ultimoAtendimento: p.last_visit_at ?? "", + proximoAtendimento: p.next_appointment_at ?? "", + vip: Boolean(p.vip ?? false), + convenio: p.convenio ?? "", + status: p.status ?? undefined, + })); + setPatients(mapped); + setCurrentPage(1); // Resetar para a primeira página ao carregar + } catch (e: any) { + setError(e?.message || "Erro ao buscar pacientes"); + setPatients([]); + } finally { + setLoading(false); + } }, []); 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]); + fetchPacientes(); + }, [fetchPacientes]); const handleDeletePatient = async (patientId: string) => { - // Remove from current list (client-side deletion) try { - const res = await patientsService.delete(patientId); - - if (res) { - alert(`${res.error} ${res.message}`); - } - - setPatients((prev) => - prev.filter((p) => String(p.id) !== String(patientId)) - ); + await patientsService.delete(patientId); + // Recarrega a lista para refletir a exclusão + await fetchPacientes(); } catch (e: any) { setError(e?.message || "Erro ao deletar paciente"); } @@ -179,6 +143,50 @@ export default function PacientesPage() { return matchesSearch && matchesConvenio && matchesVip; }); + // --- LÓGICA DE PAGINAÇÃO (ADICIONADA) --- + const totalPages = Math.ceil(filteredPatients.length / itemsPerPage); + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + const currentItems = filteredPatients.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[] = []; + 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); + + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); // Resetar para a primeira página + }; + // --------------------------------------------- + + return (
@@ -193,16 +201,15 @@ export default function PacientesPage() {
-
- {/* Convênio */}
Convênio @@ -233,18 +240,18 @@ export default function PacientesPage() {
+ + {/* SELETOR DE ITENS POR PÁGINA (ADICIONADO) */}
- - Aniversariantes - - - + - Hoje - Esta semana - Este mês + 5 por página + 10 por página + 20 por página
@@ -257,30 +264,34 @@ export default function PacientesPage() {
- {error ? ( + {loading ? ( +
+ + Carregando pacientes... +
+ ) : error ? (
{`Erro ao carregar pacientes: ${error}`}
) : ( - - - - - - - + + + + + {filteredPatients.length === 0 ? ( - ) : ( - filteredPatients.map((patient) => ( + // Mapeando `currentItems` em vez de `filteredPatients` + currentItems.map((patient) => ( - - {/* 📅 PASSO 2: Aplicar a formatação de data na tabela */} - -
NomeTelefoneCidadeEstadoÚltimo atendimentoPróximo atendimentoAçõesNomeTelefoneCidadeÚltimo AtendimentoAções
- {patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} + + {patients.length === 0 ? "Nenhum paciente cadastrado." : "Nenhum paciente encontrado com os filtros aplicados."}
@@ -292,14 +303,11 @@ export default function PacientesPage() {
{patient.telefone} {patient.cidade}{patient.estado}{formatDate(patient.ultimoAtendimento)}{formatDate(patient.proximoAtendimento)} + -
Ações
+
Ações
openDetailsDialog(String(patient.id))}> @@ -329,11 +337,44 @@ export default function PacientesPage() {
)} -
- {isFetching &&
Carregando mais pacientes...
}
+ {/* COMPONENTE DE PAGINAÇÃO (ADICIONADO) */} + {totalPages > 1 && ( +
+ + + {visiblePageNumbers.map((number) => ( + + ))} + + +
+ )} + + {/* MODAIS (SEM ALTERAÇÃO) */} @@ -357,7 +398,6 @@ export default function PacientesPage() { - {/* Modal de detalhes do paciente */} @@ -369,63 +409,12 @@ export default function PacientesPage() {
{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 ?? "-"} -

+

Nome: {patientDetails.full_name}

+

CPF: {patientDetails.cpf}

+

Email: {patientDetails.email}

+

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

+

Endereço: {`${patientDetails.street ?? "-"}, ${patientDetails.neighborhood ?? "-"}, ${patientDetails.city ?? "-"} - ${patientDetails.state ?? "-"}`}

+

Criado em: {formatDate(patientDetails.created_at)}

)} diff --git a/app/manager/usuario/page.tsx b/app/manager/usuario/page.tsx index ece5c97..805fb0c 100644 --- a/app/manager/usuario/page.tsx +++ b/app/manager/usuario/page.tsx @@ -162,7 +162,7 @@ export default function UsersPage() { 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[] = []; @@ -180,13 +180,13 @@ export default function UsersPage() { 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 --- @@ -194,7 +194,7 @@ export default function UsersPage() { return (
- + {/* Header */}
@@ -210,43 +210,56 @@ export default function UsersPage() { {/* Filtro e Itens por Página */}
- - + {/* Select de Filtro por Papel - Ajustado para resetar a página */} - +
+ + Filtrar por papel + + +
{/* Select de Itens por Página */} - +
+ + Itens por página + + +
+
{/* Fim do Filtro e Itens por Página */} @@ -316,7 +329,7 @@ export default function UsersPage() { {/* Paginação ATUALIZADA */} {totalPages > 1 && (
- + {/* Botão Anterior */} ))} - + {/* Botão Próximo */} - +
)} {/* Fim da Paginação ATUALIZADA */}