forked from RiseUP/riseup-squad21
re-alteracoes esteticas
This commit is contained in:
parent
1aec6b56d0
commit
1daa664ff4
@ -7,7 +7,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -69,24 +69,20 @@ export default function DoctorsPage() {
|
|||||||
const [specialtyFilter, setSpecialtyFilter] = useState("all");
|
const [specialtyFilter, setSpecialtyFilter] = useState("all");
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
|
|
||||||
// --- Estados para Paginação (ADICIONADOS) ---
|
// --- Estados para Paginação ---
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
// ---------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
const fetchDoctors = useCallback(async () => {
|
const fetchDoctors = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data: Doctor[] = await doctorsService.list();
|
const data: Doctor[] = await doctorsService.list();
|
||||||
// Exemplo: Adicionando um status fake
|
|
||||||
const dataWithStatus = data.map((doc, index) => ({
|
const dataWithStatus = data.map((doc, index) => ({
|
||||||
...doc,
|
...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 || []);
|
setDoctors(dataWithStatus || []);
|
||||||
// IMPORTANTE: Resetar a página ao carregar novos dados
|
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Erro ao carregar lista de médicos:", e);
|
console.error("Erro ao carregar lista de médicos:", e);
|
||||||
@ -141,37 +137,23 @@ export default function DoctorsPage() {
|
|||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleEdit = (doctorId: number) => {
|
|
||||||
router.push(`/manager/home/${doctorId}/editar`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gera a lista de especialidades dinamicamente
|
|
||||||
const uniqueSpecialties = useMemo(() => {
|
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)];
|
return [...new Set(specialties)];
|
||||||
}, [doctors]);
|
}, [doctors]);
|
||||||
|
|
||||||
// Lógica de filtragem
|
|
||||||
const filteredDoctors = doctors.filter(doctor => {
|
const filteredDoctors = doctors.filter(doctor => {
|
||||||
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
|
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
|
||||||
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
|
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
|
||||||
return specialtyMatch && statusMatch;
|
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);
|
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 indexOfLastItem = currentPage * itemsPerPage;
|
||||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||||
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
|
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
|
||||||
|
|
||||||
// 3. Função para mudar de página
|
|
||||||
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
||||||
|
|
||||||
// 4. Funções para Navegação
|
|
||||||
const goToPrevPage = () => {
|
const goToPrevPage = () => {
|
||||||
setCurrentPage((prev) => Math.max(1, prev - 1));
|
setCurrentPage((prev) => Math.max(1, prev - 1));
|
||||||
};
|
};
|
||||||
@ -180,7 +162,6 @@ export default function DoctorsPage() {
|
|||||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
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 getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
|
||||||
const pages: number[] = [];
|
const pages: number[] = [];
|
||||||
const maxVisiblePages = 5;
|
const maxVisiblePages = 5;
|
||||||
@ -205,245 +186,256 @@ export default function DoctorsPage() {
|
|||||||
|
|
||||||
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
|
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
|
||||||
|
|
||||||
// Lógica para mudar itens por página, resetando para a página 1
|
|
||||||
const handleItemsPerPageChange = (value: string) => {
|
const handleItemsPerPageChange = (value: string) => {
|
||||||
setItemsPerPage(Number(value));
|
setItemsPerPage(Number(value));
|
||||||
setCurrentPage(1); // Resetar para a primeira página
|
setCurrentPage(1);
|
||||||
};
|
};
|
||||||
// ----------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManagerLayout>
|
<ManagerLayout>
|
||||||
<div className="space-y-6 px-2 sm:px-4 md:px-6">
|
<div className="space-y-6 px-2 sm:px-4 md:px-6">
|
||||||
|
|
||||||
{/* Cabeçalho */}
|
{/* Cabeçalho */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Médicos Cadastrados</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Médicos Cadastrados</h1>
|
||||||
<p className="text-sm text-gray-500">Gerencie todos os profissionais de saúde.</p>
|
<p className="text-sm text-gray-500">Gerencie todos os profissionais de saúde.</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/manager/home/novo" className="w-full sm:w-auto">
|
||||||
|
<Button className="w-full sm:w-auto bg-green-600 hover:bg-green-700">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
Adicionar Novo
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/manager/home/novo" className="w-full sm:w-auto">
|
|
||||||
<Button className="w-full sm:w-auto bg-green-600 hover:bg-green-700">
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
{/* Filtros e Itens por Página */}
|
||||||
Adicionar Novo
|
<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>
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger className="w-[160px] sm:w-[180px]">
|
||||||
|
<SelectValue placeholder="Status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
|
<SelectItem value="Ativo">Ativo</SelectItem>
|
||||||
|
<SelectItem value="Férias">Férias</SelectItem>
|
||||||
|
<SelectItem value="Inativo">Inativo</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</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)}>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="ml-auto w-full md:w-auto">
|
||||||
|
<Filter className="w-4 h-4 mr-2" />
|
||||||
|
Filtro avançado
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Filtros e Itens por Página (ATUALIZADO) */}
|
{/* Tabela de Médicos */}
|
||||||
<div className="flex flex-wrap items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200">
|
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
|
||||||
<Filter className="w-5 h-5 text-gray-400" />
|
{loading ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">
|
||||||
{/* Filtro de Especialidade */}
|
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
|
||||||
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
|
Carregando médicos...
|
||||||
<SelectTrigger className="w-[160px] sm:w-[180px]">
|
</div>
|
||||||
<SelectValue placeholder="Especialidade" />
|
) : error ? (
|
||||||
</SelectTrigger>
|
<div className="p-8 text-center text-red-600">{error}</div>
|
||||||
<SelectContent>
|
) : filteredDoctors.length === 0 ? (
|
||||||
<SelectItem value="all">Todas</SelectItem>
|
<div className="p-8 text-center text-gray-500">
|
||||||
{uniqueSpecialties.map(specialty => (
|
{doctors.length === 0
|
||||||
<SelectItem key={specialty} value={specialty}>{specialty}</SelectItem>
|
? <>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."
|
||||||
</SelectContent>
|
}
|
||||||
</Select>
|
</div>
|
||||||
|
) : (
|
||||||
{/* Filtro de Status */}
|
<div className="overflow-x-auto">
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
<table className="w-full min-w-[600px]">
|
||||||
<SelectTrigger className="w-[160px] sm:w-[180px]">
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
<SelectValue placeholder="Status" />
|
<tr>
|
||||||
</SelectTrigger>
|
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th>
|
||||||
<SelectContent>
|
<th className="text-left p-2 md:p-4 font-medium text-gray-700">CRM</th>
|
||||||
<SelectItem value="all">Todos</SelectItem>
|
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Especialidade</th>
|
||||||
<SelectItem value="Ativo">Ativo</SelectItem>
|
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Status</th>
|
||||||
<SelectItem value="Férias">Férias</SelectItem>
|
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade/Estado</th>
|
||||||
<SelectItem value="Inativo">Inativo</SelectItem>
|
<th className="text-right p-4 font-medium text-gray-700">Ações</th>
|
||||||
</SelectContent>
|
</tr>
|
||||||
</Select>
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{/* Select de Itens por Página (ADICIONADO) */}
|
{currentItems.map((doctor) => (
|
||||||
<Select
|
<tr key={doctor.id} className="hover:bg-gray-50 transition">
|
||||||
onValueChange={handleItemsPerPageChange}
|
<td className="px-4 py-3 font-medium text-gray-900">{doctor.full_name}</td>
|
||||||
defaultValue={String(itemsPerPage)}
|
<td className="px-4 py-3 text-gray-500 hidden sm:table-cell">{doctor.crm}</td>
|
||||||
>
|
<td className="px-4 py-3 text-gray-500 hidden md:table-cell">{doctor.specialty}</td>
|
||||||
<SelectTrigger className="w-[140px]">
|
<td className="px-4 py-3 text-gray-500 hidden lg:table-cell">{doctor.status || "N/A"}</td>
|
||||||
<SelectValue placeholder="Itens por pág." />
|
<td className="px-4 py-3 text-gray-500 hidden xl:table-cell">
|
||||||
</SelectTrigger>
|
{(doctor.city || doctor.state)
|
||||||
<SelectContent>
|
? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}`
|
||||||
<SelectItem value="5">5 por página</SelectItem>
|
: "N/A"}
|
||||||
<SelectItem value="10">10 por página</SelectItem>
|
</td>
|
||||||
<SelectItem value="20">20 por página</SelectItem>
|
<td className="px-4 py-3 text-right">
|
||||||
</SelectContent>
|
{/* ===== INÍCIO DA ALTERAÇÃO ===== */}
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button variant="outline" className="ml-auto w-full md:w-auto">
|
|
||||||
<Filter className="w-4 h-4 mr-2" />
|
|
||||||
Filtro avançado
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Tabela de Médicos */}
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-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="overflow-x-auto">
|
|
||||||
<table className="min-w-[700px] w-full divide-y divide-gray-200 text-sm sm:text-base">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider">Nome</th>
|
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden sm:table-cell">CRM</th>
|
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden md:table-cell">Especialidade</th>
|
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden lg:table-cell">Status</th>
|
|
||||||
<th className="px-4 py-3 text-left font-medium text-gray-500 uppercase tracking-wider hidden xl:table-cell">Cidade/Estado</th>
|
|
||||||
<th className="px-4 py-3 text-right font-medium text-gray-500 uppercase tracking-wider">Ações</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{/* Mapeia APENAS os itens da página atual (currentItems) */}
|
|
||||||
{currentItems.map((doctor) => (
|
|
||||||
<tr key={doctor.id} className="hover:bg-gray-50 transition">
|
|
||||||
<td className="px-4 py-3 font-medium text-gray-900">{doctor.full_name}</td>
|
|
||||||
<td className="px-4 py-3 text-gray-500 hidden sm:table-cell">{doctor.crm}</td>
|
|
||||||
<td className="px-4 py-3 text-gray-500 hidden md:table-cell">{doctor.specialty}</td>
|
|
||||||
<td className="px-4 py-3 text-gray-500 hidden lg:table-cell">{doctor.status || "N/A"}</td>
|
|
||||||
<td className="px-4 py-3 text-gray-500 hidden xl:table-cell">
|
|
||||||
{(doctor.city || doctor.state)
|
|
||||||
? `${doctor.city || ""}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ""}`
|
|
||||||
: "N/A"}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(doctor)} title="Visualizar Detalhes"><Eye className="h-4 w-4" /></Button>
|
|
||||||
<Button variant="outline" size="icon" onClick={() => handleEdit(doctor.id)} title="Editar"><Edit className="h-4 w-4 text-blue-600" /></Button>
|
|
||||||
<Button variant="outline" size="icon" onClick={() => openDeleteDialog(doctor.id)} title="Excluir"><Trash2 className="h-4 w-4 text-red-600" /></Button>
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0" title="Mais Ações"><MoreVertical className="h-4 w-4" /></Button>
|
<div className="text-blue-600 cursor-pointer inline-block">Ações</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem><Calendar className="mr-2 h-4 w-4" />Agendar Consulta</DropdownMenuItem>
|
<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>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
{/* ===== FIM DA ALTERAÇÃO ===== */}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Paginação (ADICIONADA) */}
|
{/* Paginação */}
|
||||||
{totalPages > 1 && (
|
{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">
|
<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">
|
||||||
|
|
||||||
{/* 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-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
|
||||||
>
|
|
||||||
{"< Anterior"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Números das Páginas */}
|
|
||||||
{visiblePageNumbers.map((number) => (
|
|
||||||
<button
|
<button
|
||||||
key={number}
|
onClick={goToPrevPage}
|
||||||
onClick={() => paginate(number)}
|
disabled={currentPage === 1}
|
||||||
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${
|
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
||||||
currentPage === number
|
>
|
||||||
|
{"< Anterior"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{visiblePageNumbers.map((number) => (
|
||||||
|
<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-green-600 text-white shadow-md border-green-600"
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||||
}`}
|
}`}
|
||||||
|
>
|
||||||
|
{number}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goToNextPage}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
||||||
>
|
>
|
||||||
{number}
|
{"Próximo >"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Botão Próximo */}
|
{/* Dialogs de Exclusão e Detalhes */}
|
||||||
<button
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
onClick={goToNextPage}
|
<AlertDialogContent>
|
||||||
disabled={currentPage === totalPages}
|
<AlertDialogHeader>
|
||||||
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
<AlertDialogTitle>Confirma a exclusão?</AlertDialogTitle>
|
||||||
>
|
<AlertDialogDescription>
|
||||||
{"Próximo >"}
|
Esta ação é irreversível e excluirá permanentemente o registro deste médico.
|
||||||
</button>
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={loading}>Cancelar</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700" disabled={loading}>
|
||||||
|
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
||||||
|
Excluir
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
</div>
|
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
)}
|
<AlertDialogContent className="max-w-[95%] sm:max-w-lg">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="text-2xl">{doctorDetails?.nome}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-left text-gray-700">
|
||||||
|
{doctorDetails && (
|
||||||
|
<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>
|
||||||
|
|
||||||
{/* Dialogs de Exclusão e Detalhes (Sem alterações) */}
|
<h3 className="font-semibold mt-4">Atendimento e Convênio</h3>
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-2 gap-x-4 text-sm">
|
||||||
<AlertDialogContent>
|
<div><strong>Convênio:</strong> {doctorDetails.convenio || 'N/A'}</div>
|
||||||
<AlertDialogHeader>
|
<div><strong>VIP:</strong> {doctorDetails.vip ? "Sim" : "Não"}</div>
|
||||||
<AlertDialogTitle>Confirma a exclusão?</AlertDialogTitle>
|
<div><strong>Status:</strong> {doctorDetails.status || 'N/A'}</div>
|
||||||
<AlertDialogDescription>
|
<div><strong>Último atendimento:</strong> {doctorDetails.ultimo_atendimento || 'N/A'}</div>
|
||||||
Esta ação é irreversível e excluirá permanentemente o registro deste médico.
|
<div><strong>Próximo atendimento:</strong> {doctorDetails.proximo_atendimento || 'N/A'}</div>
|
||||||
</AlertDialogDescription>
|
</div>
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel disabled={loading}>Cancelar</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleDelete} className="bg-red-600 hover:bg-red-700" disabled={loading}>
|
|
||||||
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
|
||||||
Excluir
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
|
|
||||||
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
|
||||||
<AlertDialogContent className="max-w-[95%] sm:max-w-lg">
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle className="text-2xl">{doctorDetails?.nome}</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription className="text-left text-gray-700">
|
|
||||||
{doctorDetails && (
|
|
||||||
<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>
|
</div>
|
||||||
|
)}
|
||||||
<h3 className="font-semibold mt-4">Atendimento e Convênio</h3>
|
{doctorDetails === null && !loading && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-2 gap-x-4 text-sm">
|
<div className="text-red-600">Detalhes não disponíveis.</div>
|
||||||
<div><strong>Convênio:</strong> {doctorDetails.convenio || 'N/A'}</div>
|
)}
|
||||||
<div><strong>VIP:</strong> {doctorDetails.vip ? "Sim" : "Não"}</div>
|
</AlertDialogDescription>
|
||||||
<div><strong>Status:</strong> {doctorDetails.status || 'N/A'}</div>
|
</AlertDialogHeader>
|
||||||
<div><strong>Último atendimento:</strong> {doctorDetails.ultimo_atendimento || 'N/A'}</div>
|
<AlertDialogFooter>
|
||||||
<div><strong>Próximo atendimento:</strong> {doctorDetails.proximo_atendimento || 'N/A'}</div>
|
<AlertDialogCancel>Fechar</AlertDialogCancel>
|
||||||
</div>
|
</AlertDialogFooter>
|
||||||
</div>
|
</AlertDialogContent>
|
||||||
)}
|
</AlertDialog>
|
||||||
{doctorDetails === null && !loading && (
|
</div>
|
||||||
<div className="text-red-600">Detalhes não disponíveis.</div>
|
|
||||||
)}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Fechar</AlertDialogCancel>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</ManagerLayout>
|
</ManagerLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -30,29 +30,22 @@ import {
|
|||||||
import ManagerLayout from "@/components/manager-layout";
|
import ManagerLayout from "@/components/manager-layout";
|
||||||
import { patientsService } from "@/services/patientsApi.mjs";
|
import { patientsService } from "@/services/patientsApi.mjs";
|
||||||
|
|
||||||
// 📅 PASSO 1: Criar uma função para formatar a data
|
|
||||||
const formatDate = (dateString: string | null | undefined): string => {
|
const formatDate = (dateString: string | null | undefined): string => {
|
||||||
// Se a data não existir, retorna um texto padrão
|
|
||||||
if (!dateString) {
|
if (!dateString) {
|
||||||
return "N/A";
|
return "N/A";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
// Verifica se a data é válida após a conversão
|
|
||||||
if (isNaN(date.getTime())) {
|
if (isNaN(date.getTime())) {
|
||||||
return "Data inválida";
|
return "Data inválida";
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = String(date.getDate()).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 month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Se houver qualquer erro na conversão, retorna um texto de erro
|
|
||||||
return "Data inválida";
|
return "Data inválida";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -63,16 +56,19 @@ export default function PacientesPage() {
|
|||||||
const [convenioFilter, setConvenioFilter] = useState("all");
|
const [convenioFilter, setConvenioFilter] = useState("all");
|
||||||
const [vipFilter, setVipFilter] = useState("all");
|
const [vipFilter, setVipFilter] = useState("all");
|
||||||
const [patients, setPatients] = useState<any[]>([]);
|
const [patients, setPatients] = useState<any[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true); // Alterado para true
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [hasNext, setHasNext] = useState(true);
|
// --- Estados de Paginação (ADICIONADOS) ---
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const observerRef = useRef<HTMLDivElement | null>(null);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
// ---------------------------------------------
|
||||||
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
|
const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
|
||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [patientDetails, setPatientDetails] = useState<any | null>(null);
|
const [patientDetails, setPatientDetails] = useState<any | null>(null);
|
||||||
|
|
||||||
const openDetailsDialog = async (patientId: string) => {
|
const openDetailsDialog = async (patientId: string) => {
|
||||||
setDetailsDialogOpen(true);
|
setDetailsDialogOpen(true);
|
||||||
setPatientDetails(null);
|
setPatientDetails(null);
|
||||||
@ -84,75 +80,43 @@ export default function PacientesPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPacientes = useCallback(
|
// --- LÓGICA DE BUSCA DE DADOS (ATUALIZADA) ---
|
||||||
async (pageToFetch: number) => {
|
const fetchPacientes = useCallback(async () => {
|
||||||
if (isFetching || !hasNext) return;
|
setLoading(true);
|
||||||
setIsFetching(true);
|
setError(null);
|
||||||
setError(null);
|
try {
|
||||||
try {
|
const res = await patientsService.list();
|
||||||
const res = await patientsService.list();
|
const mapped = res.map((p: any) => ({
|
||||||
const mapped = res.map((p: any) => ({
|
id: String(p.id ?? ""),
|
||||||
id: String(p.id ?? ""),
|
nome: p.full_name ?? "",
|
||||||
nome: p.full_name ?? "",
|
telefone: p.phone_mobile ?? p.phone1 ?? "",
|
||||||
telefone: p.phone_mobile ?? p.phone1 ?? "",
|
cidade: p.city ?? "",
|
||||||
cidade: p.city ?? "",
|
estado: p.state ?? "",
|
||||||
estado: p.state ?? "",
|
ultimoAtendimento: p.last_visit_at ?? "",
|
||||||
ultimoAtendimento: p.last_visit_at ?? "",
|
proximoAtendimento: p.next_appointment_at ?? "",
|
||||||
proximoAtendimento: p.next_appointment_at ?? "",
|
vip: Boolean(p.vip ?? false),
|
||||||
vip: Boolean(p.vip ?? false),
|
convenio: p.convenio ?? "",
|
||||||
convenio: p.convenio ?? "", // se não existir, fica vazio
|
status: p.status ?? undefined,
|
||||||
status: p.status ?? undefined,
|
}));
|
||||||
}));
|
setPatients(mapped);
|
||||||
|
setCurrentPage(1); // Resetar para a primeira página ao carregar
|
||||||
setPatients((prev) => {
|
} catch (e: any) {
|
||||||
const all = [...prev, ...mapped];
|
setError(e?.message || "Erro ao buscar pacientes");
|
||||||
const unique = Array.from(
|
setPatients([]);
|
||||||
new Map(all.map((p) => [p.id, p])).values()
|
} finally {
|
||||||
);
|
setLoading(false);
|
||||||
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
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!observerRef.current || !hasNext) return;
|
fetchPacientes();
|
||||||
const observer = new window.IntersectionObserver((entries) => {
|
}, [fetchPacientes]);
|
||||||
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) => {
|
const handleDeletePatient = async (patientId: string) => {
|
||||||
// Remove from current list (client-side deletion)
|
|
||||||
try {
|
try {
|
||||||
const res = await patientsService.delete(patientId);
|
await patientsService.delete(patientId);
|
||||||
|
// Recarrega a lista para refletir a exclusão
|
||||||
if (res) {
|
await fetchPacientes();
|
||||||
alert(`${res.error} ${res.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPatients((prev) =>
|
|
||||||
prev.filter((p) => String(p.id) !== String(patientId))
|
|
||||||
);
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Erro ao deletar paciente");
|
setError(e?.message || "Erro ao deletar paciente");
|
||||||
}
|
}
|
||||||
@ -179,6 +143,50 @@ export default function PacientesPage() {
|
|||||||
return matchesSearch && matchesConvenio && matchesVip;
|
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 (
|
return (
|
||||||
<ManagerLayout>
|
<ManagerLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -193,16 +201,15 @@ export default function PacientesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Link href="/manager/pacientes/novo">
|
<Link href="/manager/pacientes/novo">
|
||||||
<Button className="w-full md:w-auto">
|
<Button className="w-full md:w-auto bg-green-600 hover:bg-green-700">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Adicionar
|
Adicionar Novo
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border">
|
<div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border">
|
||||||
{/* Convênio */}
|
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
<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">
|
||||||
Convênio
|
Convênio
|
||||||
@ -233,18 +240,18 @@ export default function PacientesPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SELETOR DE ITENS POR PÁGINA (ADICIONADO) */}
|
||||||
<div className="flex items-center gap-2 w-full md:w-auto">
|
<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">Itens por página</span>
|
||||||
Aniversariantes
|
<Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}>
|
||||||
</span>
|
|
||||||
<Select>
|
|
||||||
<SelectTrigger className="w-full md:w-32">
|
<SelectTrigger className="w-full md:w-32">
|
||||||
<SelectValue placeholder="Selecione" />
|
<SelectValue placeholder="Itens por pág." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="today">Hoje</SelectItem>
|
<SelectItem value="5">5 por página</SelectItem>
|
||||||
<SelectItem value="week">Esta semana</SelectItem>
|
<SelectItem value="10">10 por página</SelectItem>
|
||||||
<SelectItem value="month">Este mês</SelectItem>
|
<SelectItem value="20">20 por página</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@ -257,30 +264,34 @@ export default function PacientesPage() {
|
|||||||
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200">
|
<div className="bg-white rounded-lg border border-gray-200">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{error ? (
|
{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 pacientes...
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
|
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full min-w-[600px]">
|
<table className="w-full min-w-[600px]">
|
||||||
<thead className="bg-gray-50 border-b border-gray-200">
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th>
|
<th className="text-left p-4 font-medium text-gray-700">Nome</th>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Telefone</th>
|
<th className="text-left p-4 font-medium text-gray-700">Telefone</th>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade</th>
|
<th className="text-left p-4 font-medium text-gray-700">Cidade</th>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Estado</th>
|
<th className="text-left p-4 font-medium text-gray-700">Último Atendimento</th>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Último atendimento</th>
|
<th className="text-right p-4 font-medium text-gray-700">Ações</th>
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Próximo atendimento</th>
|
|
||||||
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Ações</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredPatients.length === 0 ? (
|
{filteredPatients.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7} className="p-8 text-center text-gray-500">
|
<td colSpan={5} className="p-8 text-center text-gray-500">
|
||||||
{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."}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
filteredPatients.map((patient) => (
|
// Mapeando `currentItems` em vez de `filteredPatients`
|
||||||
|
currentItems.map((patient) => (
|
||||||
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
|
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@ -292,14 +303,11 @@ export default function PacientesPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="p-4 text-gray-600">{patient.telefone}</td>
|
<td className="p-4 text-gray-600">{patient.telefone}</td>
|
||||||
<td className="p-4 text-gray-600">{patient.cidade}</td>
|
<td className="p-4 text-gray-600">{patient.cidade}</td>
|
||||||
<td className="p-4 text-gray-600">{patient.estado}</td>
|
|
||||||
{/* 📅 PASSO 2: Aplicar a formatação de data na tabela */}
|
|
||||||
<td className="p-4 text-gray-600">{formatDate(patient.ultimoAtendimento)}</td>
|
<td className="p-4 text-gray-600">{formatDate(patient.ultimoAtendimento)}</td>
|
||||||
<td className="p-4 text-gray-600">{formatDate(patient.proximoAtendimento)}</td>
|
<td className="p-4 text-right">
|
||||||
<td className="p-4">
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="text-blue-600 cursor-pointer">Ações</div>
|
<div className="text-blue-600 cursor-pointer inline-block">Ações</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
|
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
|
||||||
@ -329,11 +337,44 @@ export default function PacientesPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)}
|
||||||
<div ref={observerRef} style={{ height: 1 }} />
|
|
||||||
{isFetching && <div className="p-4 text-center text-gray-500">Carregando mais pacientes...</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* COMPONENTE DE PAGINAÇÃO (ADICIONADO) */}
|
||||||
|
{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">
|
||||||
|
<button
|
||||||
|
onClick={goToPrevPage}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
||||||
|
>
|
||||||
|
{"< Anterior"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{visiblePageNumbers.map((number) => (
|
||||||
|
<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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{number}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goToNextPage}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
|
||||||
|
>
|
||||||
|
{"Próximo >"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MODAIS (SEM ALTERAÇÃO) */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@ -357,7 +398,6 @@ export default function PacientesPage() {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
{/* Modal de detalhes do paciente */}
|
|
||||||
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
@ -369,63 +409,12 @@ export default function PacientesPage() {
|
|||||||
<div className="text-red-600">{patientDetails.error}</div>
|
<div className="text-red-600">{patientDetails.error}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 text-left">
|
<div className="space-y-2 text-left">
|
||||||
<p>
|
<p><strong>Nome:</strong> {patientDetails.full_name}</p>
|
||||||
<strong>Nome:</strong> {patientDetails.full_name}
|
<p><strong>CPF:</strong> {patientDetails.cpf}</p>
|
||||||
</p>
|
<p><strong>Email:</strong> {patientDetails.email}</p>
|
||||||
<p>
|
<p><strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}</p>
|
||||||
<strong>CPF:</strong> {patientDetails.cpf}
|
<p><strong>Endereço:</strong> {`${patientDetails.street ?? "-"}, ${patientDetails.neighborhood ?? "-"}, ${patientDetails.city ?? "-"} - ${patientDetails.state ?? "-"}`}</p>
|
||||||
</p>
|
<p><strong>Criado em:</strong> {formatDate(patientDetails.created_at)}</p>
|
||||||
<p>
|
|
||||||
<strong>Email:</strong> {patientDetails.email}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Nome social:</strong> {patientDetails.social_name ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Sexo:</strong> {patientDetails.sex ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Peso:</strong> {patientDetails.weight_kg ?? "-"}
|
|
||||||
{patientDetails.weight_kg ? "kg" : ""}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Altura:</strong> {patientDetails.height_m ?? "-"}
|
|
||||||
{patientDetails.height_m ? "m" : ""}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>IMC:</strong> {patientDetails.bmi ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Endereço:</strong> {patientDetails.street ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Cidade:</strong> {patientDetails.city ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Estado:</strong> {patientDetails.state ?? "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>CEP:</strong> {patientDetails.cep ?? "-"}
|
|
||||||
</p>
|
|
||||||
{/* 📅 PASSO 3: Aplicar a formatação de data no modal */}
|
|
||||||
<p>
|
|
||||||
<strong>Criado em:</strong> {formatDate(patientDetails.created_at)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Atualizado em:</strong> {formatDate(patientDetails.updated_at)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Id:</strong> {patientDetails.id ?? "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|||||||
@ -210,43 +210,56 @@ export default function UsersPage() {
|
|||||||
|
|
||||||
{/* Filtro e Itens por Página */}
|
{/* Filtro e Itens por Página */}
|
||||||
<div className="flex flex-wrap items-center gap-3 bg-white p-4 rounded-lg border border-gray-200">
|
<div className="flex flex-wrap items-center gap-3 bg-white p-4 rounded-lg border border-gray-200">
|
||||||
<Filter className="w-5 h-5 text-gray-400" />
|
|
||||||
|
|
||||||
{/* Select de Filtro por Papel - Ajustado para resetar a página */}
|
{/* Select de Filtro por Papel - Ajustado para resetar a página */}
|
||||||
<Select
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
onValueChange={(value) => {
|
<span className="text-sm font-medium text-foreground">
|
||||||
setSelectedRole(value);
|
Filtrar por papel
|
||||||
setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro
|
</span>
|
||||||
}}
|
<Select
|
||||||
value={selectedRole}
|
onValueChange={(value) => {
|
||||||
>
|
setSelectedRole(value);
|
||||||
<SelectTrigger className="w-[180px]">
|
setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro
|
||||||
<SelectValue placeholder="Filtrar por papel" />
|
}}
|
||||||
</SelectTrigger>
|
value={selectedRole}>
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Todos</SelectItem>
|
<SelectTrigger className="w-[180px]">
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectValue placeholder="Filtrar por papel" />
|
||||||
<SelectItem value="gestor">Gestor</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="medico">Médico</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="secretaria">Secretária</SelectItem>
|
<SelectItem value="all">Todos</SelectItem>
|
||||||
<SelectItem value="user">Usuário</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="gestor">Gestor</SelectItem>
|
||||||
</Select>
|
<SelectItem value="medico">Médico</SelectItem>
|
||||||
|
<SelectItem value="secretaria">Secretária</SelectItem>
|
||||||
|
<SelectItem value="user">Usuário</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Select de Itens por Página */}
|
{/* Select de Itens por Página */}
|
||||||
<Select
|
<div className="flex items-center gap-2 w-full md:w-auto">
|
||||||
onValueChange={handleItemsPerPageChange}
|
<span className="text-sm font-medium text-foreground">
|
||||||
defaultValue={String(itemsPerPage)}
|
Itens por página
|
||||||
>
|
</span>
|
||||||
<SelectTrigger className="w-[140px]">
|
<Select
|
||||||
<SelectValue placeholder="Itens por pág." />
|
onValueChange={handleItemsPerPageChange}
|
||||||
</SelectTrigger>
|
defaultValue={String(itemsPerPage)}
|
||||||
<SelectContent>
|
>
|
||||||
<SelectItem value="5">5 por página</SelectItem>
|
<SelectTrigger className="w-[140px]">
|
||||||
<SelectItem value="10">10 por página</SelectItem>
|
<SelectValue placeholder="Itens por pág." />
|
||||||
<SelectItem value="20">20 por página</SelectItem>
|
</SelectTrigger>
|
||||||
</SelectContent>
|
<SelectContent>
|
||||||
</Select>
|
<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>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" className="ml-auto w-full md:w-auto">
|
||||||
|
<Filter className="w-4 h-4 mr-2" />
|
||||||
|
Filtro avançado
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* Fim do Filtro e Itens por Página */}
|
{/* Fim do Filtro e Itens por Página */}
|
||||||
|
|
||||||
@ -331,11 +344,10 @@ export default function UsersPage() {
|
|||||||
<button
|
<button
|
||||||
key={number}
|
key={number}
|
||||||
onClick={() => paginate(number)}
|
onClick={() => paginate(number)}
|
||||||
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${
|
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number
|
||||||
currentPage === number
|
? "bg-green-600 text-white shadow-md border-green-600"
|
||||||
? "bg-green-600 text-white shadow-md border-green-600"
|
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
||||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200"
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{number}
|
{number}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user