Ajuste De Tabelas

This commit is contained in:
GagoDuBroca 2025-11-10 20:44:42 -03:00
parent 3549cab396
commit 96b8b62d6a
5 changed files with 768 additions and 544 deletions

View File

@ -1,368 +1,432 @@
// app/doctor/pacientes/page.tsx (assumindo a localização)
"use client";
import { useEffect, useState, useCallback } from "react";
import DoctorLayout from "@/components/doctor-layout";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
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,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
interface Paciente {
id: string;
nome: string;
telefone: string;
cidade: string;
estado: string;
ultimoAtendimento?: string;
proximoAtendimento?: string;
email?: string;
birth_date?: string;
cpf?: string;
blood_type?: string;
weight_kg?: number;
height_m?: number;
street?: string;
number?: string;
complement?: string;
neighborhood?: string;
cep?: string;
id: string;
nome: string;
telefone: string;
cidade: string;
estado: string;
ultimoAtendimento?: string;
proximoAtendimento?: string;
email?: string;
birth_date?: string;
cpf?: string;
blood_type?: string;
weight_kg?: number;
height_m?: number;
street?: string;
number?: string;
complement?: string;
neighborhood?: string;
cep?: string;
}
export default function PacientesPage() {
const [pacientes, setPacientes] = useState<Paciente[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedPatient, setSelectedPatient] = useState<Paciente | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [pacientes, setPacientes] = useState<Paciente[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedPatient, setSelectedPatient] = useState<Paciente | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
// --- Lógica de Paginação INÍCIO ---
const [itemsPerPage, setItemsPerPage] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
// --- 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 totalPages = Math.ceil(pacientes.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// Funções de Navegação
const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
};
// 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);
const goToNextPage = () => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
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 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);
// 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 ---
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);
};
const handleOpenModal = (patient: Paciente) => {
setSelectedPatient(patient);
setIsModalOpen(true);
};
const handleCloseModal = () => {
setSelectedPatient(null);
setIsModalOpen(false);
};
const handleCloseModal = () => {
setSelectedPatient(null);
setIsModalOpen(false);
};
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 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 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 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 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 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",
}));
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);
}
}, []);
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(() => {
fetchPacientes();
}, [fetchPacientes]);
useEffect(() => {
fetchPacientes();
}, [fetchPacientes]);
return (
<DoctorLayout>
<div className="space-y-6 px-2 sm:px-4 md:px-6">
{/* Cabeçalho */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-foreground">Pacientes</h1>
<p className="text-muted-foreground text-sm sm:text-base">
Lista de pacientes vinculados
</p>
</div>
{/* Adicione um seletor de itens por página ao lado de um botão de 'Novo Paciente' se aplicável */}
<div className="flex gap-3">
<Select
onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)}
>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Itens por pág." />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5 por página</SelectItem>
<SelectItem value="10">10 por página</SelectItem>
<SelectItem value="20">20 por página</SelectItem>
</SelectContent>
</Select>
<Link href="/doctor/pacientes/novo">
<Button variant="default" className="bg-green-600 hover:bg-green-700">
Novo Paciente
</Button>
</Link>
</div>
</div>
return (
<DoctorLayout>
<div className="space-y-6 px-2 sm:px-4 md:px-6">
{/* Cabeçalho */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> {/* Ajustado para flex-col em telas pequenas */}
<div>
<h1 className="text-2xl font-bold text-foreground">Pacientes</h1>
<p className="text-muted-foreground text-sm sm:text-base">
Lista de pacientes vinculados
</p>
</div>
{/* Controles de filtro e novo paciente */}
{/* Alterado para que o Select e o Link ocupem a largura total em telas pequenas e fiquem lado a lado em telas maiores */}
<div className="flex flex-wrap gap-3 mt-4 sm:mt-0 w-full sm:w-auto justify-start sm:justify-end">
<Select
onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)}
>
<SelectTrigger className="w-full sm:w-[140px]">
<SelectValue placeholder="Itens por pág." />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5 por página</SelectItem>
<SelectItem value="10">10 por página</SelectItem>
<SelectItem value="20">20 por página</SelectItem>
</SelectContent>
</Select>
<Link href="/doctor/pacientes/novo" className="w-full sm:w-auto">
<Button variant="default" className="bg-green-600 hover:bg-green-700 w-full sm:w-auto">
Novo Paciente
</Button>
</Link>
</div>
</div>
<div className="bg-card rounded-lg border border-border overflow-hidden shadow-md">
<div className="overflow-x-auto">
<table className="min-w-[600px] w-full">
<thead className="bg-muted border-b border-border">
<tr>
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Nome</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden md:table-cell">
Telefone
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
Cidade
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
Estado
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
Último atendimento
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
Próximo atendimento
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Ações</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={7} className="p-6 text-muted-foreground text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
Carregando pacientes...
</td>
</tr>
) : error ? (
<tr>
<td colSpan={7} className="p-6 text-red-600 text-center">{`Erro: ${error}`}</td>
</tr>
) : pacientes.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-muted-foreground">
Nenhum paciente encontrado
</td>
</tr>
) : (
currentItems.map((p) => (
<tr
key={p.id}
className="border-b border-border hover:bg-accent/40 transition-colors"
>
<td className="p-3 sm:p-4">{p.nome}</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden md:table-cell">
{p.telefone}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
{p.cidade}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
{p.estado}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
{p.ultimoAtendimento}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
{p.proximoAtendimento}
</td>
<td className="p-3 sm:p-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="text-primary hover:underline text-sm sm:text-base">
Ações
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" />
Laudos
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
<Calendar className="w-4 h-4 mr-2" />
Ver agenda
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
// 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 focus:bg-red-50 focus:text-red-600"
<div className="bg-card rounded-lg border border-border overflow-hidden shadow-md">
{/* Tabela para Telas Médias e Grandes */}
<div className="overflow-x-auto hidden md:block"> {/* Esconde em telas pequenas */}
<table className="min-w-[600px] w-full">
<thead className="bg-muted border-b border-border">
<tr>
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Nome</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground">
Telefone
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
Cidade
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
Estado
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
Último atendimento
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
Próximo atendimento
</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Ações</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={7} className="p-6 text-muted-foreground text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
Carregando pacientes...
</td>
</tr>
) : error ? (
<tr>
<td colSpan={7} className="p-6 text-red-600 text-center">{`Erro: ${error}`}</td>
</tr>
) : pacientes.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-muted-foreground">
Nenhum paciente encontrado
</td>
</tr>
) : (
currentItems.map((p) => (
<tr
key={p.id}
className="border-b border-border hover:bg-accent/40 transition-colors"
>
<td className="p-3 sm:p-4">{p.nome}</td>
<td className="p-3 sm:p-4 text-muted-foreground">
{p.telefone}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
{p.cidade}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
{p.estado}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
{p.ultimoAtendimento}
</td>
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
{p.proximoAtendimento}
</td>
<td className="p-3 sm:p-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="text-primary hover:underline text-sm sm:text-base">
Ações
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" />
Laudos
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
<Calendar className="w-4 h-4 mr-2" />
Ver agenda
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
setPacientes(newPacientes);
alert(`Paciente ID: ${p.id} excluído`);
}}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Layout em Cards/Lista para Telas Pequenas */}
<div className="md:hidden divide-y divide-border"> {/* Visível apenas em telas pequenas */}
{loading ? (
<div className="p-6 text-muted-foreground text-center">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
Carregando pacientes...
</div>
) : error ? (
<div className="p-6 text-red-600 text-center">{`Erro: ${error}`}</div>
) : pacientes.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
Nenhum paciente encontrado
</div>
) : (
currentItems.map((p) => (
<div key={p.id} className="flex items-center justify-between p-4 hover:bg-accent/40 transition-colors">
<div className="flex-1 min-w-0 pr-4"> {/* Adicionado padding à direita */}
<div className="text-base font-semibold text-foreground break-words"> {/* Aumentado a fonte e break-words para evitar corte do nome */}
{p.nome || "—"}
</div>
{/* Removido o 'truncate' e adicionado 'break-words' no telefone */}
<div className="text-sm text-muted-foreground break-words">
Telefone: **{p.telefone || "N/A"}**
</div>
</div>
<div className="ml-4 flex-shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Eye className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" />
Laudos
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
<Calendar className="w-4 h-4 mr-2" />
Ver agenda
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
setPacientes(newPacientes);
alert(`Paciente ID: ${p.id} excluído`);
}}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))
)}
</div>
{/* Paginação */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 border-t border-border p-4 bg-muted/40">
{/* Botão Anterior */}
<button
onClick={goToPrevPage}
disabled={currentPage === 1}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50 disabled:cursor-not-allowed border border-border"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{"< Anterior"}
</button>
{/* Paginação ATUALIZADA */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 border-t border-border p-4 bg-muted/40">
{/* Botão Anterior */}
<button
onClick={goToPrevPage}
disabled={currentPage === 1}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50 disabled:cursor-not-allowed border border-border"
>
{"< Anterior"}
</button>
{/* Números das Páginas */}
{visiblePageNumbers.map((number) => (
<button
key={number}
onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-border ${
currentPage === number
? "bg-green-600 text-primary-foreground shadow-md border-green-600"
: "bg-secondary text-secondary-foreground hover:bg-secondary/80"
}`}
>
{number}
</button>
))}
{/* Números das Páginas */}
{visiblePageNumbers.map((number) => (
<button
key={number}
onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-border ${
currentPage === number
? "bg-green-600 text-primary-foreground shadow-md border-green-600"
: "bg-secondary text-secondary-foreground hover:bg-secondary/80"
}`}
>
{number}
</button>
))}
{/* Botão Próximo */}
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50 disabled:cursor-not-allowed border border-border"
>
{"Próximo >"}
</button>
{/* Botão Próximo */}
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:opacity-50 disabled:cursor-not-allowed border border-border"
>
{"Próximo >"}
</button>
</div>
)}
</div>
</div>
)}
{/* Fim da Paginação ATUALIZADA */}
</div>
</div>
<PatientDetailsModal
patient={selectedPatient}
isOpen={isModalOpen}
onClose={handleCloseModal}
/>
</DoctorLayout>
);
<PatientDetailsModal
patient={selectedPatient}
isOpen={isModalOpen}
onClose={handleCloseModal}
/>
</DoctorLayout>
);
}

View File

@ -1,13 +1,13 @@
"use client";
import React, { useEffect, useState, useCallback, useMemo } from "react"
import React, { useEffect, useState, useCallback, useMemo } from "react";
import ManagerLayout from "@/components/manager-layout";
import Link from "next/link"
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 { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react"
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, Loader2 } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
@ -17,11 +17,10 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
} from "@/components/ui/alert-dialog";
import { doctorsService } from "services/doctorsApi.mjs";
interface Doctor {
id: number;
full_name: string;
@ -33,7 +32,6 @@ interface Doctor {
status?: string;
}
interface DoctorDetails {
nome: string;
crm: string;
@ -41,11 +39,11 @@ interface DoctorDetails {
contato: {
celular?: string;
telefone1?: string;
}
};
endereco: {
cidade?: string;
estado?: string;
}
};
convenio?: string;
vip?: boolean;
status?: string;
@ -80,7 +78,7 @@ export default function DoctorsPage() {
const data: Doctor[] = await doctorsService.list();
const dataWithStatus = data.map((doc, index) => ({
...doc,
status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo"
status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo",
}));
setDoctors(dataWithStatus || []);
setCurrentPage(1);
@ -93,12 +91,10 @@ export default function DoctorsPage() {
}
}, []);
useEffect(() => {
fetchDoctors();
}, [fetchDoctors]);
const openDetailsDialog = async (doctor: Doctor) => {
setDetailsDialogOpen(true);
setDoctorDetails({
@ -115,7 +111,6 @@ export default function DoctorsPage() {
});
};
const handleDelete = async () => {
if (doctorToDeleteId === null) return;
setLoading(true);
@ -138,11 +133,11 @@ export default function DoctorsPage() {
};
const uniqueSpecialties = useMemo(() => {
const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean);
const specialties = doctors.map((doctor) => doctor.specialty).filter(Boolean);
return [...new Set(specialties)];
}, [doctors]);
const filteredDoctors = doctors.filter(doctor => {
const filteredDoctors = doctors.filter((doctor) => {
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
return specialtyMatch && statusMatch;
@ -191,11 +186,9 @@ export default function DoctorsPage() {
setCurrentPage(1);
};
return (
<ManagerLayout>
<div className="space-y-6 px-2 sm:px-4 md:px-6">
{/* Cabeçalho */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
@ -204,29 +197,26 @@ export default function DoctorsPage() {
</div>
</div>
{/* Filtros e Itens por Página */}
<div className="flex flex-wrap items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200">
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Especialidade
</span>
<span className="text-sm font-medium text-foreground">Especialidade</span>
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
<SelectTrigger className="w-[160px] sm:w-[180px]">
<SelectValue placeholder="Especialidade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas</SelectItem>
{uniqueSpecialties.map(specialty => (
<SelectItem key={specialty} value={specialty}>{specialty}</SelectItem>
{uniqueSpecialties.map((specialty) => (
<SelectItem key={specialty} value={specialty}>
{specialty}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Status
</span>
<span className="text-sm font-medium text-foreground">Status</span>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[160px] sm:w-[180px]">
<SelectValue placeholder="Status" />
@ -240,12 +230,8 @@ export default function DoctorsPage() {
</Select>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
Itens por página
</span>
<Select
onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)}>
<span className="text-sm font-medium text-foreground">Itens por página</span>
<Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="Itens por pág." />
</SelectTrigger>
@ -262,9 +248,8 @@ export default function DoctorsPage() {
</Button>
</div>
{/* Tabela de Médicos */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
{/* Tabela de Médicos (Visível em Telas Médias e Maiores) */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden hidden md:block">
{loading ? (
<div className="p-8 text-center text-gray-500">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
@ -287,8 +272,8 @@ export default function DoctorsPage() {
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">CRM</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Especialidade</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Status</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade/Estado</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700 hidden lg:table-cell">Status</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700 hidden xl:table-cell">Cidade/Estado</th>
<th className="text-right p-4 font-medium text-gray-700">Ações</th>
</tr>
</thead>
@ -305,7 +290,6 @@ export default function DoctorsPage() {
: "N/A"}
</td>
<td className="px-4 py-3 text-right">
{/* ===== INÍCIO DA ALTERAÇÃO ===== */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-blue-600 cursor-pointer inline-block">Ações</div>
@ -331,7 +315,6 @@ export default function DoctorsPage() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* ===== FIM DA ALTERAÇÃO ===== */}
</td>
</tr>
))}
@ -341,6 +324,61 @@ export default function DoctorsPage() {
)}
</div>
{/* Cards de Médicos (Visível Apenas em Telas Pequenas) */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md p-4 block md:hidden">
{loading ? (
<div className="p-8 text-center text-gray-500">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
Carregando médicos...
</div>
) : error ? (
<div className="p-8 text-center text-red-600">{error}</div>
) : filteredDoctors.length === 0 ? (
<div className="p-8 text-center text-gray-500">
{doctors.length === 0
? <>Nenhum médico cadastrado. <Link href="/manager/home/novo" className="text-green-600 hover:underline">Adicione um novo</Link>.</>
: "Nenhum médico encontrado com os filtros aplicados."
}
</div>
) : (
<div className="space-y-4">
{currentItems.map((doctor) => (
<div key={doctor.id} className="bg-white-50 rounded-lg p-4 flex justify-between items-center border border-white-200">
<div>
<div className="font-semibold text-gray-900">{doctor.full_name}</div>
<div className="text-sm text-gray-600">{doctor.specialty}</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-blue-600 cursor-pointer inline-block">Ações</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(doctor)}>
<Eye className="mr-2 h-4 w-4" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/manager/home/${doctor.id}/editar`}>
<Edit className="mr-2 h-4 w-4" />
Editar
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Calendar className="mr-2 h-4 w-4" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(doctor.id)}>
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
)}
</div>
{/* Paginação */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 bg-white rounded-lg border border-gray-200 shadow-md">
@ -356,10 +394,11 @@ export default function DoctorsPage() {
<button
key={number}
onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number
? "bg-green-600 text-white shadow-md border-green-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${
currentPage === number
? "bg-green-600 text-white shadow-md border-green-600"
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
}`}
>
{number}
</button>
@ -380,9 +419,7 @@ export default function DoctorsPage() {
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirma a exclusão?</AlertDialogTitle>
<AlertDialogDescription>
Esta ação é irreversível e excluirá permanentemente o registro deste médico.
</AlertDialogDescription>
<AlertDialogDescription>Esta ação é irreversível e excluirá permanentemente o registro deste médico.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Cancelar</AlertDialogCancel>
@ -403,25 +440,41 @@ export default function DoctorsPage() {
<div className="space-y-3 text-left">
<h3 className="font-semibold mt-2">Informações Principais</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-2 gap-x-4 text-sm">
<div><strong>CRM:</strong> {doctorDetails.crm}</div>
<div><strong>Especialidade:</strong> {doctorDetails.especialidade}</div>
<div><strong>Celular:</strong> {doctorDetails.contato.celular || 'N/A'}</div>
<div><strong>Localização:</strong> {`${doctorDetails.endereco.cidade || 'N/A'}/${doctorDetails.endereco.estado || 'N/A'}`}</div>
<div>
<strong>CRM:</strong> {doctorDetails.crm}
</div>
<div>
<strong>Especialidade:</strong> {doctorDetails.especialidade}
</div>
<div>
<strong>Celular:</strong> {doctorDetails.contato.celular || "N/A"}
</div>
<div>
<strong>Localização:</strong> {`${doctorDetails.endereco.cidade || "N/A"}/${doctorDetails.endereco.estado || "N/A"}`}
</div>
</div>
<h3 className="font-semibold mt-4">Atendimento e Convênio</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-2 gap-x-4 text-sm">
<div><strong>Convênio:</strong> {doctorDetails.convenio || 'N/A'}</div>
<div><strong>VIP:</strong> {doctorDetails.vip ? "Sim" : "Não"}</div>
<div><strong>Status:</strong> {doctorDetails.status || 'N/A'}</div>
<div><strong>Último atendimento:</strong> {doctorDetails.ultimo_atendimento || 'N/A'}</div>
<div><strong>Próximo atendimento:</strong> {doctorDetails.proximo_atendimento || 'N/A'}</div>
<div>
<strong>Convênio:</strong> {doctorDetails.convenio || "N/A"}
</div>
<div>
<strong>VIP:</strong> {doctorDetails.vip ? "Sim" : "Não"}
</div>
<div>
<strong>Status:</strong> {doctorDetails.status || "N/A"}
</div>
<div>
<strong>Último atendimento:</strong> {doctorDetails.ultimo_atendimento || "N/A"}
</div>
<div>
<strong>Próximo atendimento:</strong> {doctorDetails.proximo_atendimento || "N/A"}
</div>
</div>
</div>
)}
{doctorDetails === null && !loading && (
<div className="text-red-600">Detalhes não disponíveis.</div>
)}
{doctorDetails === null && !loading && <div className="text-red-600">Detalhes não disponíveis.</div>}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@ -1,4 +1,3 @@
"use client";
import { useState, useEffect, useCallback } from "react";
@ -156,6 +155,7 @@ export default function PacientesPage() {
</div>
{/* Bloco de Filtros (Responsividade APLICADA) */}
{/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */}
<div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border">
<Filter className="w-5 h-5 text-gray-400" />
@ -165,14 +165,15 @@ export default function PacientesPage() {
placeholder="Buscar por nome ou telefone..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full sm:flex-grow sm:min-w-[150px] p-2 border rounded-md text-sm"
// w-full no mobile, depois flex-grow para ocupar o espaço disponível
className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm"
/>
{/* Convênio - Ocupa metade da linha no mobile */}
<div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[200px]">
{/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">Convênio</span>
<Select value={convenioFilter} onValueChange={setConvenioFilter}>
<SelectTrigger className="w-full sm:w-40">
<SelectTrigger className="w-full sm:w-40"> {/* w-full para mobile, w-40 para sm+ */}
<SelectValue placeholder="Convênio" />
</SelectTrigger>
<SelectContent>
@ -185,11 +186,11 @@ export default function PacientesPage() {
</Select>
</div>
{/* VIP - Ocupa a outra metade da linha no mobile */}
<div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[150px]">
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
<Select value={vipFilter} onValueChange={setVipFilter}>
<SelectTrigger className="w-full sm:w-32">
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */}
<SelectValue placeholder="VIP" />
</SelectTrigger>
<SelectContent>
@ -200,16 +201,17 @@ export default function PacientesPage() {
</Select>
</div>
{/* Aniversariantes - Vai para a linha de baixo no mobile, ocupando 100% */}
{/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */}
<Button variant="outline" className="w-full md:w-auto md:ml-auto">
<Calendar className="w-4 h-4 mr-2" />
Aniversariantes
</Button>
</div>
{/* Tabela (Responsividade APLICADA) */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
<div className="overflow-x-auto">
{/* --- 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+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block">
<div className="overflow-x-auto"> {/* Permite rolagem horizontal se a tabela for muito larga */}
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : loading ? (
@ -217,18 +219,14 @@ export default function PacientesPage() {
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
</div>
) : (
// min-w ajustado para responsividade
<table className="w-full min-w-[650px] md:min-w-[900px]">
<table className="w-full min-w-[650px]"> {/* min-w para evitar que a tabela se contraia demais */}
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th>
{/* Coluna oculta em telas muito pequenas */}
{/* Ajustes de visibilidade de colunas para diferentes breakpoints */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Telefone</th>
{/* Coluna oculta em telas pequenas e muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">Cidade / Estado</th>
{/* Coluna oculta em telas muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Convênio</th>
{/* Colunas ocultas em telas médias, pequenas e muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Último atendimento</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Próximo atendimento</th>
<th className="text-left p-4 font-medium text-gray-700 w-[5%]">Ações</th>
@ -257,7 +255,6 @@ export default function PacientesPage() {
</span>
</div>
</td>
{/* Aplicação das classes de visibilidade */}
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.telefone}</td>
<td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td>
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.convenio}</td>
@ -300,53 +297,109 @@ export default function PacientesPage() {
</table>
)}
</div>
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
{/* Renderização dos botões de número de página (Limitando a 5) */}
<div className="flex space-x-2"> {/* Increased space-x for more separation */}
{/* Botão Anterior */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
&lt; Anterior
</Button>
{/* --- SEÇÃO DE CARDS (VISÍVEL APENAS EM TELAS MENORES QUE MD) --- */}
{/* Garantir que os cards apareçam em telas menores e se escondam em MD+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md p-4 block md:hidden">
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : loading ? (
<div className="p-6 text-center text-gray-500 flex items-center justify-center">
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
</div>
) : filteredPatients.length === 0 ? (
<div className="p-8 text-center text-gray-500">
{allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"}
</div>
) : (
<div className="space-y-4">
{currentPatients.map((patient) => (
<div key={patient.id} className="bg-gray-50 rounded-lg p-4 flex flex-col sm:flex-row justify-between items-start sm:items-center border border-gray-200">
<div className="flex-grow mb-2 sm:mb-0">
<div className="font-semibold text-lg text-gray-900 flex items-center">
{patient.nome}
{patient.vip && (
<span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">VIP</span>
)}
</div>
<div className="text-sm text-gray-600">Telefone: {patient.telefone}</div>
<div className="text-sm text-gray-600">Convênio: {patient.convenio}</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full"><Button variant="outline" className="w-full">Ações</Button></div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg" // Changed to "lg" for larger buttons
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
<DropdownMenuItem asChild>
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
<Edit className="w-4 h-4 mr-2" />
Editar
</Link>
</DropdownMenuItem>
{/* Botão Próximo */}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
Próximo &gt;
</Button>
</div>
<DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
)}
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<div className="flex space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg"
>
&lt; Anterior
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg"
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg"
>
Próximo &gt;
</Button>
</div>
</div>
)}
{/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
{/* ... (AlertDialog de Exclusão) ... */}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
@ -362,7 +415,6 @@ export default function PacientesPage() {
</AlertDialog>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
{/* ... (AlertDialog de Detalhes) ... */}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
@ -376,7 +428,7 @@ export default function PacientesPage() {
<div className="text-red-600 p-4">{patientDetails.error}</div>
) : (
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="font-semibold">Nome Completo</p>
<p>{patientDetails.full_name}</p>
@ -412,7 +464,7 @@ export default function PacientesPage() {
</div>
<div className="border-t pt-4 mt-4">
<h3 className="font-semibold mb-2">Endereço</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="font-semibold">Rua</p>
<p>{`${patientDetails.street}, ${patientDetails.number}`}</p>

View File

@ -1,4 +1,3 @@
// app/manager/usuario/page.tsx
"use client";
import React, { useEffect, useState, useCallback } from "react";
@ -22,8 +21,8 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { api, login } from "services/api.mjs";
import { usersService } from "services/usersApi.mjs";
import { api, login } from "services/api.mjs"; // Verifique o caminho correto para 'api' e 'login'
import { usersService } from "services/usersApi.mjs"; // Verifique o caminho correto para 'usersApi.mjs'
interface FlatUser {
id: string;
@ -49,17 +48,15 @@ export default function UsersPage() {
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(
null
);
// Ajuste 1: Definir 'all' como valor inicial para garantir que todos os usuários sejam exibidos por padrão.
const [selectedRole, setSelectedRole] = useState<string>("all");
// --- Lógica de Paginação INÍCIO ---
const [itemsPerPage, setItemsPerPage] = useState(10);
const [currentPage, setCurrentPage] = useState(1);
// 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);
};
// --- Lógica de Paginação FIM ---
@ -95,8 +92,7 @@ export default function UsersPage() {
});
setUsers(mapped);
setCurrentPage(1); // Resetar a página após carregar
console.log("[fetchUsers] mapped count:", mapped.length);
setCurrentPage(1);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Veja console.");
@ -123,9 +119,7 @@ export default function UsersPage() {
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);
@ -138,23 +132,19 @@ export default function UsersPage() {
}
};
// 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));
};
@ -163,15 +153,13 @@ export default function UsersPage() {
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 maxVisiblePages = 5;
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);
@ -188,8 +176,6 @@ export default function UsersPage() {
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
// --- Fim das Funções e Lógica de Navegação ADICIONADAS ---
return (
<ManagerLayout>
@ -213,17 +199,17 @@ export default function UsersPage() {
{/* Select de Filtro por Papel - Ajustado para resetar a página */}
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
<span className="text-sm font-medium text-foreground whitespace-nowrap">
Filtrar por papel
</span>
<Select
onValueChange={(value) => {
setSelectedRole(value);
setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro
setCurrentPage(1);
}}
value={selectedRole}>
<SelectTrigger className="w-[180px]">
<SelectTrigger className="w-full sm:w-[180px]"> {/* w-full para mobile, w-[180px] para sm+ */}
<SelectValue placeholder="Filtrar por papel" />
</SelectTrigger>
<SelectContent>
@ -239,14 +225,14 @@ export default function UsersPage() {
{/* Select de Itens por Página */}
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">
<span className="text-sm font-medium text-foreground whitespace-nowrap">
Itens por página
</span>
<Select
onValueChange={handleItemsPerPageChange}
defaultValue={String(itemsPerPage)}
>
<SelectTrigger className="w-[140px]">
<SelectTrigger className="w-full sm:w-[140px]"> {/* w-full para mobile, w-[140px] para sm+ */}
<SelectValue placeholder="Itens por pág." />
</SelectTrigger>
<SelectContent>
@ -263,7 +249,7 @@ export default function UsersPage() {
</div>
{/* Fim do Filtro e Itens por Página */}
{/* Tabela */}
{/* Tabela/Lista */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-x-auto">
{loading ? (
<div className="p-8 text-center text-gray-500">
@ -278,10 +264,10 @@ export default function UsersPage() {
</div>
) : (
<>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 hidden md:table-header-group">
{/* Tabela para Telas Médias e Grandes */}
<table className="min-w-full divide-y divide-gray-200 hidden md:table">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th>
@ -290,15 +276,8 @@ export default function UsersPage() {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{/* Usando currentItems para a paginação */}
{currentItems.map((u) => (
<tr
key={u.id}
className="flex flex-col md:table-row md:flex-row border-b md:border-0 hover:bg-gray-50"
>
<td className="px-6 py-4 text-sm text-gray-500 break-all md:whitespace-nowrap">
{u.id}
</td>
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm text-gray-900">
{u.full_name}
</td>
@ -326,7 +305,33 @@ export default function UsersPage() {
</tbody>
</table>
{/* Paginação ATUALIZADA */}
{/* Layout em Cards/Lista para Telas Pequenas */}
<div className="md:hidden divide-y divide-gray-200">
{currentItems.map((u) => (
<div key={u.id} className="flex items-center justify-between p-4 hover:bg-gray-50">
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">
{u.full_name || "—"}
</div>
<div className="text-sm text-gray-500 capitalize">
{u.role || "—"}
</div>
</div>
<div className="ml-4 flex-shrink-0">
<Button
variant="outline"
size="icon"
onClick={() => openDetailsDialog(u)}
title="Visualizar"
>
<Eye className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{/* Paginação */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 border-t border-gray-200">
@ -364,7 +369,6 @@ export default function UsersPage() {
</div>
)}
{/* Fim da Paginação ATUALIZADA */}
</>
)}
</div>
@ -401,7 +405,6 @@ export default function UsersPage() {
<strong>Roles:</strong>{" "}
{userDetails.roles?.join(", ")}
</div>
{/* Melhoria na visualização das permissões no modal */}
<div className="pt-2">
<strong className="block mb-1">Permissões:</strong>
<ul className="list-disc list-inside space-y-0.5 text-sm">

View File

@ -164,7 +164,7 @@ export default function PacientesPage() {
</div>
{/* Bloco de Filtros (Responsividade APLICADA) */}
<div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border">
<div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border">
<Filter className="w-5 h-5 text-gray-400" />
{/* Busca - Ocupa 100% no mobile, depois cresce */}
@ -173,14 +173,15 @@ export default function PacientesPage() {
placeholder="Buscar por nome ou telefone..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full sm:flex-grow sm:min-w-[150px] p-2 border rounded-md text-sm"
// w-full no mobile, depois flex-grow para ocupar o espaço disponível
className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm"
/>
{/* Convênio - Ocupa metade da linha no mobile */}
<div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[200px]">
{/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">Convênio</span>
<Select value={convenioFilter} onValueChange={setConvenioFilter}>
<SelectTrigger className="w-full sm:w-40">
<SelectTrigger className="w-full sm:w-40"> {/* w-full para mobile, w-40 para sm+ */}
<SelectValue placeholder="Convênio" />
</SelectTrigger>
<SelectContent>
@ -193,11 +194,11 @@ export default function PacientesPage() {
</Select>
</div>
{/* VIP - Ocupa a outra metade da linha no mobile */}
<div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[150px]">
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
<Select value={vipFilter} onValueChange={setVipFilter}>
<SelectTrigger className="w-full sm:w-32">
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */}
<SelectValue placeholder="VIP" />
</SelectTrigger>
<SelectContent>
@ -208,16 +209,17 @@ export default function PacientesPage() {
</Select>
</div>
{/* Aniversariantes - Vai para a linha de baixo no mobile, ocupando 100% */}
{/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */}
<Button variant="outline" className="w-full md:w-auto md:ml-auto">
<Calendar className="w-4 h-4 mr-2" />
Aniversariantes
</Button>
</div>
{/* Tabela (Responsividade APLICADA) */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
<div className="overflow-x-auto">
{/* --- 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+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block">
<div className="overflow-x-auto"> {/* Permite rolagem horizontal se a tabela for muito larga */}
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : loading ? (
@ -225,18 +227,14 @@ export default function PacientesPage() {
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
</div>
) : (
// min-w ajustado para responsividade
<table className="w-full min-w-[650px] md:min-w-[900px]">
<table className="w-full min-w-[650px]"> {/* min-w para evitar que a tabela se contraia demais */}
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th>
{/* Coluna oculta em telas muito pequenas */}
{/* Ajustes de visibilidade de colunas para diferentes breakpoints */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Telefone</th>
{/* Coluna oculta em telas pequenas e muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">Cidade / Estado</th>
{/* Coluna oculta em telas muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Convênio</th>
{/* Colunas ocultas em telas médias, pequenas e muito pequenas */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Último atendimento</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Próximo atendimento</th>
<th className="text-left p-4 font-medium text-gray-700 w-[5%]">Ações</th>
@ -265,7 +263,6 @@ export default function PacientesPage() {
</span>
</div>
</td>
{/* Aplicação das classes de visibilidade */}
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.telefone}</td>
<td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td>
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.convenio}</td>
@ -308,53 +305,109 @@ export default function PacientesPage() {
</table>
)}
</div>
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
{/* Renderização dos botões de número de página (Limitando a 5) */}
<div className="flex space-x-2"> {/* Increased space-x for more separation */}
{/* Botão Anterior */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
&lt; Anterior
</Button>
{/* --- SEÇÃO DE CARDS (VISÍVEL APENAS EM TELAS MENORES QUE MD) --- */}
{/* Garantir que os cards apareçam em telas menores e se escondam em MD+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md p-4 block md:hidden">
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : loading ? (
<div className="p-6 text-center text-gray-500 flex items-center justify-center">
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
</div>
) : filteredPatients.length === 0 ? (
<div className="p-8 text-center text-gray-500">
{allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"}
</div>
) : (
<div className="space-y-4">
{currentPatients.map((patient) => (
<div key={patient.id} className="bg-gray-50 rounded-lg p-4 flex flex-col sm:flex-row justify-between items-start sm:items-center border border-gray-200">
<div className="flex-grow mb-2 sm:mb-0">
<div className="font-semibold text-lg text-gray-900 flex items-center">
{patient.nome}
{patient.vip && (
<span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">VIP</span>
)}
</div>
<div className="text-sm text-gray-600">Telefone: {patient.telefone}</div>
<div className="text-sm text-gray-600">Convênio: {patient.convenio}</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full"><Button variant="outline" className="w-full">Ações</Button></div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg" // Changed to "lg" for larger buttons
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
<DropdownMenuItem asChild>
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
<Edit className="w-4 h-4 mr-2" />
Editar
</Link>
</DropdownMenuItem>
{/* Botão Próximo */}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
Próximo &gt;
</Button>
</div>
<DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
)}
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<div className="flex space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg"
>
&lt; Anterior
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg"
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg"
>
Próximo &gt;
</Button>
</div>
</div>
)}
{/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
{/* ... (AlertDialog de Exclusão) ... */}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
@ -370,7 +423,6 @@ export default function PacientesPage() {
</AlertDialog>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
{/* ... (AlertDialog de Detalhes) ... */}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
@ -384,7 +436,7 @@ export default function PacientesPage() {
<div className="text-red-600 p-4">{patientDetails.error}</div>
) : (
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="font-semibold">Nome Completo</p>
<p>{patientDetails.full_name}</p>
@ -420,7 +472,7 @@ export default function PacientesPage() {
</div>
<div className="border-t pt-4 mt-4">
<h3 className="font-semibold mb-2">Endereço</h3>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="font-semibold">Rua</p>
<p>{`${patientDetails.street}, ${patientDetails.number}`}</p>