Aba de lista, e detalhes dos pacientes

This commit is contained in:
GagoDuBroca 2025-11-05 23:15:54 -03:00
parent 2f12067e9d
commit 1aec6b56d0
6 changed files with 1734 additions and 1231 deletions

View File

@ -21,6 +21,8 @@ import { Eye, Edit, Calendar, Trash2 } from "lucide-react";
import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal"; import { AvailabilityEditModal } from "@/components/ui/availability-edit-modal";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
// ... (Interfaces de tipo omitidas para brevidade, pois não foram alteradas)
interface UserPermissions { interface UserPermissions {
isAdmin: boolean; isAdmin: boolean;
isManager: boolean; isManager: boolean;
@ -52,32 +54,32 @@ interface UserData {
} }
type Doctor = { type Doctor = {
id: string; id: string;
user_id: string | null; user_id: string | null;
crm: string; crm: string;
crm_uf: string; crm_uf: string;
specialty: string; specialty: string;
full_name: string; full_name: string;
cpf: string; cpf: string;
email: string; email: string;
phone_mobile: string | null; phone_mobile: string | null;
phone2: string | null; phone2: string | null;
cep: string | null; cep: string | null;
street: string | null; street: string | null;
number: string | null; number: string | null;
complement: string | null; complement: string | null;
neighborhood: string | null; neighborhood: string | null;
city: string | null; city: string | null;
state: string | null; state: string | null;
birth_date: string | null; birth_date: string | null;
rg: string | null; rg: string | null;
active: boolean; active: boolean;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
created_by: string; created_by: string;
updated_by: string | null; updated_by: string | null;
max_days_in_advance: number; max_days_in_advance: number;
rating: number | null; rating: number | null;
} }
type Availability = { type Availability = {
@ -335,43 +337,45 @@ export default function AvailabilityPage() {
<h2 className="text-lg font-semibold text-gray-900 mb-6">Dados </h2> <h2 className="text-lg font-semibold text-gray-900 mb-6">Dados </h2>
<div className="space-y-6"> <div className="space-y-6">
<div className="grid md:grid-cols-3 gap-4"> {/* **AJUSTE DE RESPONSIVIDADE: DIAS DA SEMANA** */}
<div> <div>
<Label className="text-sm font-medium text-gray-700">Dia Da Semana</Label> <Label className="text-sm font-medium text-gray-700">Dia Da Semana</Label>
<div className="flex gap-4 mt-2 flex-nowrap"> {/* O antigo 'flex gap-4 mt-2 flex-nowrap' foi substituído por um grid responsivo: */}
<label className="flex items-center gap-2"> <div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-7 gap-x-4 gap-y-2 mt-2">
<input type="radio" name="weekday" value="monday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Segunda-Feira</span> <input type="radio" name="weekday" value="monday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Segunda</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="tuesday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Terça-Feira</span> <input type="radio" name="weekday" value="tuesday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Terça</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="wednesday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Quarta-Feira</span> <input type="radio" name="weekday" value="wednesday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Quarta</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="thursday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Quinta-Feira</span> <input type="radio" name="weekday" value="thursday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Quinta</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="friday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Sexta-Feira</span> <input type="radio" name="weekday" value="friday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Sexta</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="saturday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Sábado</span> <input type="radio" name="weekday" value="saturday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Sábado</span>
<label className="flex items-center gap-2"> </label>
<input type="radio" name="weekday" value="sunday" className="text-blue-600" /> <label className="flex items-center gap-1">
<span className="whitespace-nowrap text-sm">Domingo</span> <input type="radio" name="weekday" value="sunday" className="text-blue-600" />
</label> <span className="whitespace-nowrap text-sm">Domingo</span>
</div> </label>
</div> </div>
</div> </div>
<div className="grid md:grid-cols-5 gap-6"> {/* **AJUSTE DE RESPONSIVIDADE: HORÁRIO E DURAÇÃO** */}
{/* Ajustado para 1 coluna em móvel, 2 em tablet e 5 em desktop (mantendo o que já existia com ajustes) */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
<div> <div>
<Label htmlFor="horarioEntrada" className="text-sm font-medium text-gray-700"> <Label htmlFor="horarioEntrada" className="text-sm font-medium text-gray-700">
Horario De Entrada Horario De Entrada
@ -390,6 +394,7 @@ export default function AvailabilityPage() {
</Label> </Label>
<Input type="number" id="duracaoConsulta" name="duracaoConsulta" required className="mt-1" /> <Input type="number" id="duracaoConsulta" name="duracaoConsulta" required className="mt-1" />
</div> </div>
{/* O Select de modalidade fica fora deste grid para ocupar uma linha inteira em telas menores, como no original, garantindo clareza */}
</div> </div>
<div> <div>
@ -409,53 +414,56 @@ export default function AvailabilityPage() {
</div> </div>
</div> </div>
<div className="flex justify-between gap-4"> {/* **AJUSTE DE RESPONSIVIDADE: BOTÕES DE AÇÃO** */}
<Link href="/doctor/disponibilidade/excecoes"> {/* Alinha à direita em telas maiores e empilha (com o botão primário no final) em telas menores */}
<Button variant="default">Adicionar Exceção</Button> <div className="flex flex-col-reverse sm:flex-row sm:justify-between gap-4">
<Link href="/doctor/disponibilidade/excecoes" className="w-full sm:w-auto">
<Button variant="default" className="w-full sm:w-auto">Adicionar Exceção</Button>
</Link> </Link>
<div> <div className="flex gap-4 w-full sm:w-auto">
<Link href="/doctor/dashboard"> <Link href="/doctor/dashboard" className="w-full sm:w-auto">
<Button variant="outline">Cancelar</Button> <Button variant="outline" className="w-full sm:w-auto">Cancelar</Button>
</Link> </Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700"> <Button type="submit" className="bg-green-600 hover:bg-green-700 w-full sm:w-auto">
Salvar Disponibilidade Salvar Disponibilidade
</Button> </Button>
</div> </div>
</div> </div>
</form> </form>
{/* **AJUSTE DE RESPONSIVIDADE: CARD DE HORÁRIO SEMANAL** */}
<div> <div>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Horário Semanal</CardTitle> <CardTitle>Horário Semanal</CardTitle>
<CardDescription>Confira ou altere a sua disponibilidade da semana</CardDescription> <CardDescription>Confira ou altere a sua disponibilidade da semana</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 grid md:grid-cols-7 gap-2"> {/* Define um grid responsivo para os dias da semana (1 coluna em móvel, 2 em pequeno, 3 em médio e 7 em telas grandes) */}
<CardContent className="space-y-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7 gap-2">
{["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => {
const times = schedule[day] || []; const times = schedule[day] || [];
return ( return (
<div key={day} className="space-y-4"> <div key={day} className="space-y-4">
<div className="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg"> <div className="flex flex-col items-center justify-between p-3 bg-blue-50 rounded-lg h-full">
<div> <p className="font-medium capitalize text-center mb-2">{weekdaysPT[day]}</p>
<p className="font-medium capitalize">{weekdaysPT[day]}</p> <div className="text-center w-full">
</div>
<div className="text-center">
{times.length > 0 ? ( {times.length > 0 ? (
times.map((t, i) => ( times.map((t, i) => (
<div key={i}> <div key={i}>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<p className="text-sm text-gray-600 hover:text-accent-foreground hover:bg-gray-200"> <p className="text-sm text-gray-600 cursor-pointer p-1 rounded hover:text-accent-foreground hover:bg-gray-200 transition-colors duration-150">
{formatTime(t.start)} <br /> {formatTime(t.end)} {formatTime(t.start)} - {formatTime(t.end)}
</p> </p>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(t, day)}> <DropdownMenuItem onClick={() => handleOpenModal(t, day)}>
<Eye className="w-4 h-4 mr-2" /> <Edit className="w-4 h-4 mr-2" />
Editar Editar
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => openDeleteDialog(t, day)} onClick={() => openDeleteDialog(t, day)}
className="text-red-600"> className="text-red-600 focus:bg-red-50 focus:text-red-600">
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Excluir Excluir
</DropdownMenuItem> </DropdownMenuItem>
@ -474,6 +482,8 @@ export default function AvailabilityPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
{/* AlertDialog e Modal de Edição (não precisam de grandes ajustes de layout, apenas garantindo que os componentes sejam responsivos internamente) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>

View File

@ -1,6 +1,7 @@
// app/doctor/pacientes/page.tsx (assumindo a localização)
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
import DoctorLayout from "@/components/doctor-layout"; import DoctorLayout from "@/components/doctor-layout";
import Link from "next/link"; import Link from "next/link";
import { import {
@ -9,9 +10,17 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Eye, Edit, Calendar, Trash2 } from "lucide-react"; import { Eye, Edit, Calendar, Trash2, Loader2 } from "lucide-react";
import { api } from "@/services/api.mjs"; import { api } from "@/services/api.mjs";
import { PatientDetailsModal } from "@/components/ui/patient-details-modal"; import { PatientDetailsModal } from "@/components/ui/patient-details-modal";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
interface Paciente { interface Paciente {
id: string; id: string;
@ -41,6 +50,60 @@ export default function PacientesPage() {
const [selectedPatient, setSelectedPatient] = useState<Paciente | null>(null); const [selectedPatient, setSelectedPatient] = useState<Paciente | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
// --- Lógica de Paginação INÍCIO ---
const [itemsPerPage, setItemsPerPage] = useState(5);
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(pacientes.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// Funções de Navegação
const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
};
const goToNextPage = () => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
// Lógica para gerar os números das páginas visíveis (máximo de 5)
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
const pages: number[] = [];
const maxVisiblePages = 5;
const halfRange = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfRange);
let endPage = Math.min(totalPages, currentPage + halfRange);
if (endPage - startPage + 1 < maxVisiblePages) {
if (endPage === totalPages) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
if (startPage === 1) {
endPage = Math.min(totalPages, maxVisiblePages);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
// Lógica para mudar itens por página, resetando para a página 1
const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(Number(value));
setCurrentPage(1);
};
// --- Lógica de Paginação FIM ---
const handleOpenModal = (patient: Paciente) => { const handleOpenModal = (patient: Paciente) => {
setSelectedPatient(patient); setSelectedPatient(patient);
setIsModalOpen(true); setIsModalOpen(true);
@ -51,92 +114,132 @@ export default function PacientesPage() {
setIsModalOpen(false); setIsModalOpen(false);
}; };
const formatDate = (dateString: string) => { const formatDate = (dateString: string | null | undefined) => {
if (!dateString) return ""; if (!dateString) return "N/A";
const date = new Date(dateString); try {
return new Intl.DateTimeFormat('pt-BR').format(date); const date = new Date(dateString);
return new Intl.DateTimeFormat("pt-BR").format(date);
} catch (e) {
return dateString; // Retorna o string original se o formato for inválido
}
}; };
const [itemsPerPage, setItemsPerPage] = useState(5); const fetchPacientes = useCallback(async () => {
const [currentPage, setCurrentPage] = useState(1); try {
setLoading(true);
setError(null);
const json = await api.get("/rest/v1/patients");
const items = Array.isArray(json)
? json
: Array.isArray(json?.data)
? json.data
: [];
const indexOfLastItem = currentPage * itemsPerPage; const mapped: Paciente[] = items.map((p: any) => ({
const indexOfFirstItem = indexOfLastItem - itemsPerPage; id: String(p.id ?? ""),
const currentItems = pacientes.slice(indexOfFirstItem, indexOfLastItem); nome: p.full_name ?? "—",
telefone: p.phone_mobile ?? "N/A",
cidade: p.city ?? "N/A",
estado: p.state ?? "N/A",
ultimoAtendimento: formatDate(p.created_at),
proximoAtendimento: "N/A", // Necessita de lógica de agendamento real
email: p.email ?? "N/A",
birth_date: p.birth_date ?? "N/A",
cpf: p.cpf ?? "N/A",
blood_type: p.blood_type ?? "N/A",
weight_kg: p.weight_kg ?? 0,
height_m: p.height_m ?? 0,
street: p.street ?? "N/A",
number: p.number ?? "N/A",
complement: p.complement ?? "N/A",
neighborhood: p.neighborhood ?? "N/A",
cep: p.cep ?? "N/A",
}));
const paginate = (pageNumber: number) => setCurrentPage(pageNumber); setPacientes(mapped);
setCurrentPage(1); // Resetar a página ao carregar novos dados
} catch (e: any) {
console.error("Erro ao carregar pacientes:", e);
setError(e?.message || "Erro ao carregar pacientes");
} finally {
setLoading(false);
}
}, []);
useEffect(() => { useEffect(() => {
async function fetchPacientes() {
try {
setLoading(true);
setError(null);
const json = await api.get("/rest/v1/patients");
const items = Array.isArray(json) ? json : (Array.isArray(json?.data) ? json.data : []);
const mapped = items.map((p: any) => ({
id: String(p.id ?? ""),
nome: p.full_name ?? "",
telefone: p.phone_mobile ?? "",
cidade: p.city ?? "",
estado: p.state ?? "",
ultimoAtendimento: formatDate(p.created_at) ?? "",
proximoAtendimento: "",
email: p.email ?? "",
birth_date: p.birth_date ?? "",
cpf: p.cpf ?? "",
blood_type: p.blood_type ?? "",
weight_kg: p.weight_kg ?? 0,
height_m: p.height_m ?? 0,
street: p.street ?? "",
number: p.number ?? "",
complement: p.complement ?? "",
neighborhood: p.neighborhood ?? "",
cep: p.cep ?? "",
}));
setPacientes(mapped);
} catch (e: any) {
setError(e?.message || "Erro ao carregar pacientes");
} finally {
setLoading(false);
}
}
fetchPacientes(); fetchPacientes();
}, []); }, [fetchPacientes]);
return ( return (
<DoctorLayout> <DoctorLayout>
<div className="space-y-6"> <div className="space-y-6 px-2 sm:px-4 md:px-6">
<div> {/* Cabeçalho */}
<h1 className="text-2xl font-bold text-foreground">Pacientes</h1> <div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-muted-foreground">Lista de pacientes vinculados</p> <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-primary hover:bg-primary/90">
Novo Paciente
</Button>
</Link>
</div>
</div> </div>
<div className="bg-card rounded-lg border border-border">
<div className="bg-card rounded-lg border border-border overflow-hidden shadow-md">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="min-w-[600px] w-full">
<thead className="bg-muted border-b border-border"> <thead className="bg-muted border-b border-border">
<tr> <tr>
<th className="text-left p-4 font-medium text-foreground">Nome</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground">Nome</th>
<th className="text-left p-4 font-medium text-foreground">Telefone</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden md:table-cell">
<th className="text-left p-4 font-medium text-foreground">Cidade</th> Telefone
<th className="text-left p-4 font-medium text-foreground">Estado</th> </th>
<th className="text-left p-4 font-medium text-foreground">Último atendimento</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
<th className="text-left p-4 font-medium text-foreground">Próximo atendimento</th> Cidade
<th className="text-left p-4 font-medium text-foreground">Ações</th> </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> </tr>
</thead> </thead>
<tbody> <tbody>
{loading ? ( {loading ? (
<tr> <tr>
<td colSpan={7} className="p-6 text-muted-foreground"> <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... Carregando pacientes...
</td> </td>
</tr> </tr>
) : error ? ( ) : error ? (
<tr> <tr>
<td colSpan={7} className="p-6 text-red-600">{`Erro: ${error}`}</td> <td colSpan={7} className="p-6 text-red-600 text-center">{`Erro: ${error}`}</td>
</tr> </tr>
) : pacientes.length === 0 ? ( ) : pacientes.length === 0 ? (
<tr> <tr>
@ -146,17 +249,32 @@ export default function PacientesPage() {
</tr> </tr>
) : ( ) : (
currentItems.map((p) => ( currentItems.map((p) => (
<tr key={p.id} className="border-b border-border hover:bg-accent"> <tr
<td className="p-4">{p.nome}</td> key={p.id}
<td className="p-4 text-muted-foreground">{p.telefone}</td> className="border-b border-border hover:bg-accent/40 transition-colors"
<td className="p-4 text-muted-foreground">{p.cidade}</td> >
<td className="p-4 text-muted-foreground">{p.estado}</td> <td className="p-3 sm:p-4">{p.nome}</td>
<td className="p-4 text-muted-foreground">{p.ultimoAtendimento}</td> <td className="p-3 sm:p-4 text-muted-foreground hidden md:table-cell">
<td className="p-4 text-muted-foreground">{p.proximoAtendimento}</td> {p.telefone}
<td className="p-4"> </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> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="text-primary hover:underline">Ações</button> <button className="text-primary hover:underline text-sm sm:text-base">
Ações
</button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}> <DropdownMenuItem onClick={() => handleOpenModal(p)}>
@ -164,7 +282,7 @@ export default function PacientesPage() {
Ver detalhes Ver detalhes
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link href={`/doctor/medicos/${p.id}/laudos`}> <Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" /> <Edit className="w-4 h-4 mr-2" />
Laudos Laudos
</Link> </Link>
@ -175,11 +293,14 @@ export default function PacientesPage() {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
const newPacientes = pacientes.filter((pac) => pac.id !== p.id) // Simulação de exclusão (A exclusão real deve ser feita via API)
setPacientes(newPacientes) const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
alert(`Paciente ID: ${p.id} excluído`) setPacientes(newPacientes);
alert(`Paciente ID: ${p.id} excluído`);
// Necessário chamar a API de exclusão aqui
}} }}
className="text-red-600"> className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
Excluir Excluir
</DropdownMenuItem> </DropdownMenuItem>
@ -192,19 +313,51 @@ export default function PacientesPage() {
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="flex justify-center space-x-2 mt-4 p-4">
{Array.from({ length: Math.ceil(pacientes.length / itemsPerPage) }, (_, i) => ( {/* 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 <button
key={i} onClick={goToPrevPage}
onClick={() => paginate(i + 1)} disabled={currentPage === 1}
className={`px-4 py-2 rounded-md ${currentPage === i + 1 ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground'}`} 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"
> >
{i + 1} {"< Anterior"}
</button> </button>
))}
</div> {/* 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-primary text-primary-foreground shadow-md border-primary"
: "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>
</div>
)}
{/* Fim da Paginação ATUALIZADA */}
</div> </div>
</div> </div>
<PatientDetailsModal <PatientDetailsModal
patient={selectedPatient} patient={selectedPatient}
isOpen={isModalOpen} isOpen={isModalOpen}

View File

@ -1,6 +1,5 @@
"use client"; "use client";
// -> ADICIONADO: useMemo para otimizar a criação da lista de especialidades
import React, { useEffect, useState, useCallback, useMemo } from "react" import React, { useEffect, useState, useCallback, useMemo } from "react"
import ManagerLayout from "@/components/manager-layout"; import ManagerLayout from "@/components/manager-layout";
import Link from "next/link" import Link from "next/link"
@ -10,329 +9,441 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { 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, MoreVertical, Loader2 } from "lucide-react"
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { doctorsService } from "services/doctorsApi.mjs"; import { doctorsService } from "services/doctorsApi.mjs";
interface Doctor { interface Doctor {
id: number; id: number;
full_name: string; full_name: string;
specialty: string; specialty: string;
crm: string; crm: string;
phone_mobile: string | null; phone_mobile: string | null;
city: string | null; city: string | null;
state: string | null; state: string | null;
// -> ADICIONADO: Campo 'status' para que o filtro funcione. Sua API precisa retornar este dado. status?: string;
status?: string;
} }
interface DoctorDetails { interface DoctorDetails {
nome: string; nome: string;
crm: string; crm: string;
especialidade: string; especialidade: string;
contato: { contato: {
celular?: string; celular?: string;
telefone1?: string; telefone1?: string;
} }
endereco: { endereco: {
cidade?: string; cidade?: string;
estado?: string; estado?: string;
} }
convenio?: string; convenio?: string;
vip?: boolean; vip?: boolean;
status?: string; status?: string;
ultimo_atendimento?: string; ultimo_atendimento?: string;
proximo_atendimento?: string; proximo_atendimento?: string;
error?: string; error?: string;
} }
export default function DoctorsPage() { export default function DoctorsPage() {
const router = useRouter(); const router = useRouter();
const [doctors, setDoctors] = useState<Doctor[]>([]); const [doctors, setDoctors] = useState<Doctor[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [doctorDetails, setDoctorDetails] = useState<DoctorDetails | null>(null); const [doctorDetails, setDoctorDetails] = useState<DoctorDetails | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [doctorToDeleteId, setDoctorToDeleteId] = useState<number | null>(null); const [doctorToDeleteId, setDoctorToDeleteId] = useState<number | null>(null);
// -> PASSO 1: Criar estados para os filtros // --- Estados para Filtros ---
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) ---
const [itemsPerPage, setItemsPerPage] = useState(10);
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 para o filtro funcionar. O ideal é que isso venha da API. // 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 || []);
} catch (e: any) { // IMPORTANTE: Resetar a página ao carregar novos dados
console.error("Erro ao carregar lista de médicos:", e); setCurrentPage(1);
setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); } catch (e: any) {
setDoctors([]); console.error("Erro ao carregar lista de médicos:", e);
} finally { setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API.");
setLoading(false); setDoctors([]);
} } finally {
}, []); setLoading(false);
}
}, []);
useEffect(() => { useEffect(() => {
fetchDoctors(); fetchDoctors();
}, [fetchDoctors]); }, [fetchDoctors]);
const openDetailsDialog = async (doctor: Doctor) => { const openDetailsDialog = async (doctor: Doctor) => {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setDoctorDetails({ setDoctorDetails({
nome: doctor.full_name, nome: doctor.full_name,
crm: doctor.crm, crm: doctor.crm,
especialidade: doctor.specialty, especialidade: doctor.specialty,
contato: { celular: doctor.phone_mobile ?? undefined }, contato: { celular: doctor.phone_mobile ?? undefined },
endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined },
status: doctor.status || "Ativo", // Usa o status do médico status: doctor.status || "Ativo",
convenio: "Particular", convenio: "Particular",
vip: false, vip: false,
ultimo_atendimento: "N/A", ultimo_atendimento: "N/A",
proximo_atendimento: "N/A", proximo_atendimento: "N/A",
});
};
const handleDelete = async () => {
if (doctorToDeleteId === null) return;
setLoading(true);
try {
await doctorsService.delete(doctorToDeleteId);
setDeleteDialogOpen(false);
setDoctorToDeleteId(null);
await fetchDoctors();
} catch (e) {
console.error("Erro ao excluir:", e);
alert("Erro ao excluir médico.");
} finally {
setLoading(false);
}
};
const openDeleteDialog = (doctorId: number) => {
setDoctorToDeleteId(doctorId);
setDeleteDialogOpen(true);
};
const handleEdit = (doctorId: number) => {
router.push(`/manager/home/${doctorId}/editar`);
};
// Gera a lista de especialidades dinamicamente
const uniqueSpecialties = useMemo(() => {
const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean);
return [...new Set(specialties)];
}, [doctors]);
// Lógica de filtragem
const filteredDoctors = doctors.filter(doctor => {
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
return specialtyMatch && statusMatch;
}); });
};
// --- Lógica de Paginação (ADICIONADA) ---
// 1. Definição do total de páginas com base nos itens FILTRADOS
const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage);
// 2. Cálculo dos itens a serem exibidos na página atual (dos itens FILTRADOS)
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
// 3. Função para mudar de página
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// 4. Funções para Navegação
const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
};
const goToNextPage = () => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
// 5. Lógica para gerar os números das páginas visíveis
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
const pages: number[] = [];
const maxVisiblePages = 5;
const halfRange = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfRange);
let endPage = Math.min(totalPages, currentPage + halfRange);
if (endPage - startPage + 1 < maxVisiblePages) {
if (endPage === totalPages) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
if (startPage === 1) {
endPage = Math.min(totalPages, maxVisiblePages);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
// Lógica para mudar itens por página, resetando para a página 1
const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(Number(value));
setCurrentPage(1); // Resetar para a primeira página
};
// ----------------------------------------------------
const handleDelete = async () => { return (
if (doctorToDeleteId === null) return; <ManagerLayout>
setLoading(true); <div className="space-y-6 px-2 sm:px-4 md:px-6">
try {
await doctorsService.delete(doctorToDeleteId);
setDeleteDialogOpen(false);
setDoctorToDeleteId(null);
await fetchDoctors();
} catch (e) {
console.error("Erro ao excluir:", e);
alert("Erro ao excluir médico.");
} finally {
setLoading(false);
}
};
const openDeleteDialog = (doctorId: number) => { {/* Cabeçalho */}
setDoctorToDeleteId(doctorId); <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
setDeleteDialogOpen(true); <div>
}; <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>
</div>
const handleEdit = (doctorId: number) => { <Link href="/manager/home/novo" className="w-full sm:w-auto">
router.push(`/manager/home/${doctorId}/editar`); <Button className="w-full sm:w-auto bg-green-600 hover:bg-green-700">
}; <Plus className="w-4 h-4 mr-2" />
Adicionar Novo
// -> MELHORIA: Gera a lista de especialidades dinamicamente </Button>
const uniqueSpecialties = useMemo(() => { </Link>
const specialties = doctors.map(doctor => doctor.specialty).filter(Boolean);
return [...new Set(specialties)];
}, [doctors]);
// -> PASSO 3: Aplicar a lógica de filtragem
const filteredDoctors = doctors.filter(doctor => {
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
return specialtyMatch && statusMatch;
});
return (
<ManagerLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<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>
</div>
</div>
{/* -> PASSO 2: Conectar os estados aos componentes Select <- */}
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
<span className="text-sm font-medium text-foreground">Especialidades</span>
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Especialidade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas</SelectItem>
{uniqueSpecialties.map(specialty => (
<SelectItem key={specialty} value={specialty}>{specialty}</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-sm font-medium text-foreground">Status</span>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="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>
<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 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> </div>
) : error ? (
<div className="p-8 text-center text-red-600">
{error}
</div>
// -> Atualizado para usar a lista filtrada
) : 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-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nome</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CRM</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Especialidade</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Celular</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cidade/Estado</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{/* -> ATUALIZADO para mapear a lista filtrada */}
{filteredDoctors.map((doctor) => (
<tr key={doctor.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{doctor.full_name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{doctor.crm}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{doctor.specialty}</td>
{/* Coluna de Status adicionada para visualização */}
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{doctor.status}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{doctor.phone_mobile || "N/A"}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{(doctor.city || doctor.state) ? `${doctor.city || ''}${doctor.city && doctor.state ? '/' : ''}${doctor.state || ''}` : "N/A"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<DropdownMenu> {/* Filtros e Itens por Página (ATUALIZADO) */}
<DropdownMenuTrigger asChild> <div className="flex flex-wrap items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200">
<div className="text-blue-600">Ações</div> <Filter className="w-5 h-5 text-gray-400" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end"> {/* Filtro de Especialidade */}
<DropdownMenuItem onClick={() => openDetailsDialog(doctor)}> <Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
<Eye className="w-4 h-4 mr-2" /> <SelectTrigger className="w-[160px] sm:w-[180px]">
Ver detalhes <SelectValue placeholder="Especialidade" />
</DropdownMenuItem> </SelectTrigger>
<DropdownMenuItem onClick={() => handleEdit(doctor.id)}> <SelectContent>
<Edit className="w-4 h-4 mr-2" /> <SelectItem value="all">Todas</SelectItem>
Editar {uniqueSpecialties.map(specialty => (
</DropdownMenuItem> <SelectItem key={specialty} value={specialty}>{specialty}</SelectItem>
<DropdownMenuItem ))}
className="text-red-600 focus:text-red-700" </SelectContent>
onClick={() => openDeleteDialog(doctor.id)} </Select>
>
<Trash2 className="w-4 h-4 mr-2" /> {/* Filtro de Status */}
Excluir <Select value={statusFilter} onValueChange={setStatusFilter}>
</DropdownMenuItem> <SelectTrigger className="w-[160px] sm:w-[180px]">
</DropdownMenuContent> <SelectValue placeholder="Status" />
</DropdownMenu> </SelectTrigger>
</td> <SelectContent>
</tr> <SelectItem value="all">Todos</SelectItem>
))} <SelectItem value="Ativo">Ativo</SelectItem>
</tbody> <SelectItem value="Férias">Férias</SelectItem>
</table> <SelectItem value="Inativo">Inativo</SelectItem>
</SelectContent>
</Select>
{/* Select de Itens por Página (ADICIONADO) */}
<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>
<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>
)}
</div>
{/* ... O resto do seu código (AlertDialogs) permanece o mesmo ... */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirma a exclusão?</AlertDialogTitle>
<AlertDialogDescription>
Esta ação é irreversível e excluirá permanentemente o registro deste médico.
</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>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> {/* Tabela de Médicos */}
<AlertDialogContent> <div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
<AlertDialogHeader> {loading ? (
<AlertDialogTitle className="text-2xl">{doctorDetails?.nome}</AlertDialogTitle> <div className="p-8 text-center text-gray-500">
<AlertDialogDescription className="text-left text-gray-700"> <Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
{doctorDetails && ( Carregando médicos...
<div className="space-y-3 text-left">
<h3 className="font-semibold mt-2">Informações Principais</h3>
<div className="grid grid-cols-2 gap-y-1 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>
) : error ? (
<h3 className="font-semibold mt-4">Atendimento e Convênio</h3> <div className="p-8 text-center text-red-600">{error}</div>
<div className="grid grid-cols-2 gap-y-1 gap-x-4 text-sm"> ) : filteredDoctors.length === 0 ? (
<div><strong>Convênio:</strong> {doctorDetails.convenio || 'N/A'}</div> <div className="p-8 text-center text-gray-500">
<div><strong>VIP:</strong> {doctorDetails.vip ? "Sim" : "Não"}</div> {doctors.length === 0
<div><strong>Status:</strong> {doctorDetails.status || 'N/A'}</div> ? <>Nenhum médico cadastrado. <Link href="/manager/home/novo" className="text-green-600 hover:underline">Adicione um novo</Link>.</>
<div><strong>Último atendimento:</strong> {doctorDetails.ultimo_atendimento || 'N/A'}</div> : "Nenhum médico encontrado com os filtros aplicados."
<div><strong>Próximo atendimento:</strong> {doctorDetails.proximo_atendimento || 'N/A'}</div> }
</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>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0" title="Mais Ações"><MoreVertical className="h-4 w-4" /></Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem><Calendar className="mr-2 h-4 w-4" />Agendar Consulta</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div> </div>
</div>
)} )}
{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> {/* Paginação (ADICIONADA) */}
</ManagerLayout> {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">
{/* 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
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>
))}
{/* 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-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
>
{"Próximo >"}
</button>
</div>
)}
{/* Dialogs de Exclusão e Detalhes (Sem alterações) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirma a exclusão?</AlertDialogTitle>
<AlertDialogDescription>
Esta ação é irreversível e excluirá permanentemente o registro deste médico.
</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>
<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>
<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>
</div>
)}
{doctorDetails === null && !loading && (
<div className="text-red-600">Detalhes não disponíveis.</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Fechar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</ManagerLayout>
);
} }

View File

@ -30,34 +30,32 @@ 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";
// --- INÍCIO DA MODIFICAÇÃO --- // 📅 PASSO 1: Criar uma função para formatar a data
// PASSO 1: Criar uma função para formatar a data
const formatDate = (dateString: string | null | undefined): string => { const formatDate = (dateString: string | null | undefined): string => {
// Se a data não existir, retorna um texto padrão // Se a data não existir, retorna um texto padrão
if (!dateString) { if (!dateString) {
return "N/A"; return "N/A";
}
try {
const date = new Date(dateString);
// Verifica se a data é válida após a conversão
if (isNaN(date.getTime())) {
return "Data inválida";
} }
try { const day = String(date.getDate()).padStart(2, '0');
const date = new Date(dateString); const month = String(date.getMonth() + 1).padStart(2, '0'); // Mês é base 0, então +1
// Verifica se a data é válida após a conversão const year = date.getFullYear();
if (isNaN(date.getTime())) { const hours = String(date.getHours()).padStart(2, '0');
return "Data inválida"; const minutes = String(date.getMinutes()).padStart(2, '0');
}
const day = String(date.getDate()).padStart(2, '0'); return `${day}/${month}/${year} ${hours}:${minutes}`;
const month = String(date.getMonth() + 1).padStart(2, '0'); // Mês é base 0, então +1 } catch (error) {
const year = date.getFullYear(); // Se houver qualquer erro na conversão, retorna um texto de erro
const hours = String(date.getHours()).padStart(2, '0'); return "Data inválida";
const minutes = String(date.getMinutes()).padStart(2, '0'); }
return `${day}/${month}/${year} ${hours}:${minutes}`;
} catch (error) {
// Se houver qualquer erro na conversão, retorna um texto de erro
return "Data inválida";
}
}; };
// --- FIM DA MODIFICAÇÃO ---
export default function PacientesPage() { export default function PacientesPage() {
@ -257,86 +255,84 @@ export default function PacientesPage() {
</Button> </Button>
</div> </div>
<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 ? ( {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-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">Telefone</th> <th className="text-left p-2 md: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-2 md: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-2 md:p-4 font-medium text-gray-700">Estado</th>
<th className="text-left p-2 md: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-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">Próximo atendimento</th>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Ações</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={7} 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) => ( filteredPatients.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">
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center">
<span className="text-gray-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span> <span className="text-gray-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span>
</div> </div>
<span className="font-medium text-gray-900">{patient.nome}</span> <span className="font-medium text-gray-900">{patient.nome}</span>
</div> </div>
</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> <td className="p-4 text-gray-600">{patient.estado}</td>
{/* --- INÍCIO DA MODIFICAÇÃO --- */} {/* 📅 PASSO 2: Aplicar a formatação de data na tabela */}
{/* 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-gray-600">{formatDate(patient.proximoAtendimento)}</td> <td className="p-4">
{/* --- FIM DA MODIFICAÇÃO --- */} <DropdownMenu>
<td className="p-4"> <DropdownMenuTrigger asChild>
<DropdownMenu> <div className="text-blue-600 cursor-pointer">Ações</div>
<DropdownMenuTrigger asChild> </DropdownMenuTrigger>
<div className="text-blue-600 cursor-pointer">Ações</div> <DropdownMenuContent align="end">
</DropdownMenuTrigger> <DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<DropdownMenuContent align="end"> <Eye className="w-4 h-4 mr-2" />
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}> Ver detalhes
<Eye className="w-4 h-4 mr-2" /> </DropdownMenuItem>
Ver detalhes <DropdownMenuItem asChild>
</DropdownMenuItem> <Link href={`/manager/pacientes/${patient.id}/editar`}>
<DropdownMenuItem asChild> <Edit className="w-4 h-4 mr-2" />
<Link href={`/manager/pacientes/${patient.id}/editar`}> Editar
<Edit className="w-4 h-4 mr-2" /> </Link>
Editar </DropdownMenuItem>
</Link> <DropdownMenuItem>
</DropdownMenuItem> <Calendar className="w-4 h-4 mr-2" />
<DropdownMenuItem> Marcar consulta
<Calendar className="w-4 h-4 mr-2" /> </DropdownMenuItem>
Marcar consulta <DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
</DropdownMenuItem> <Trash2 className="w-4 h-4 mr-2" />
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}> Excluir
<Trash2 className="w-4 h-4 mr-2" /> </DropdownMenuItem>
Excluir </DropdownMenuContent>
</DropdownMenuItem> </DropdownMenu>
</DropdownMenuContent> </td>
</DropdownMenu> </tr>
</td> ))
</tr> )}
)) </tbody>
)} </table>
</tbody> )}
</table> <div ref={observerRef} style={{ height: 1 }} />
)} {isFetching && <div className="p-4 text-center text-gray-500">Carregando mais pacientes...</div>}
<div ref={observerRef} style={{ height: 1 }} /> </div>
{isFetching && <div className="p-4 text-center text-gray-500">Carregando mais pacientes...</div>} </div>
</div>
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
@ -361,87 +357,85 @@ export default function PacientesPage() {
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Modal de detalhes do paciente */} {/* Modal de detalhes do paciente */}
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle> <AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
{patientDetails === null ? ( {patientDetails === null ? (
<div className="text-gray-500">Carregando...</div> <div className="text-gray-500">Carregando...</div>
) : patientDetails?.error ? ( ) : patientDetails?.error ? (
<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} <strong>Nome:</strong> {patientDetails.full_name}
</p> </p>
<p> <p>
<strong>CPF:</strong> {patientDetails.cpf} <strong>CPF:</strong> {patientDetails.cpf}
</p> </p>
<p> <p>
<strong>Email:</strong> {patientDetails.email} <strong>Email:</strong> {patientDetails.email}
</p> </p>
<p> <p>
<strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"} <strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}
</p> </p>
<p> <p>
<strong>Nome social:</strong> {patientDetails.social_name ?? "-"} <strong>Nome social:</strong> {patientDetails.social_name ?? "-"}
</p> </p>
<p> <p>
<strong>Sexo:</strong> {patientDetails.sex ?? "-"} <strong>Sexo:</strong> {patientDetails.sex ?? "-"}
</p> </p>
<p> <p>
<strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"} <strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"}
</p> </p>
<p> <p>
<strong>Peso:</strong> {patientDetails.weight_kg ?? "-"} <strong>Peso:</strong> {patientDetails.weight_kg ?? "-"}
{patientDetails.weight_kg ? "kg" : ""} {patientDetails.weight_kg ? "kg" : ""}
</p> </p>
<p> <p>
<strong>Altura:</strong> {patientDetails.height_m ?? "-"} <strong>Altura:</strong> {patientDetails.height_m ?? "-"}
{patientDetails.height_m ? "m" : ""} {patientDetails.height_m ? "m" : ""}
</p> </p>
<p> <p>
<strong>IMC:</strong> {patientDetails.bmi ?? "-"} <strong>IMC:</strong> {patientDetails.bmi ?? "-"}
</p> </p>
<p> <p>
<strong>Endereço:</strong> {patientDetails.street ?? "-"} <strong>Endereço:</strong> {patientDetails.street ?? "-"}
</p> </p>
<p> <p>
<strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"} <strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"}
</p> </p>
<p> <p>
<strong>Cidade:</strong> {patientDetails.city ?? "-"} <strong>Cidade:</strong> {patientDetails.city ?? "-"}
</p> </p>
<p> <p>
<strong>Estado:</strong> {patientDetails.state ?? "-"} <strong>Estado:</strong> {patientDetails.state ?? "-"}
</p> </p>
<p> <p>
<strong>CEP:</strong> {patientDetails.cep ?? "-"} <strong>CEP:</strong> {patientDetails.cep ?? "-"}
</p> </p>
{/* --- INÍCIO DA MODIFICAÇÃO --- */} {/* 📅 PASSO 3: Aplicar a formatação de data no modal */}
{/* PASSO 3: Aplicar a formatação de data no modal */} <p>
<p> <strong>Criado em:</strong> {formatDate(patientDetails.created_at)}
<strong>Criado em:</strong> {formatDate(patientDetails.created_at)} </p>
</p> <p>
<p> <strong>Atualizado em:</strong> {formatDate(patientDetails.updated_at)}
<strong>Atualizado em:</strong> {formatDate(patientDetails.updated_at)} </p>
</p> <p>
{/* --- FIM DA MODIFICAÇÃO --- */} <strong>Id:</strong> {patientDetails.id ?? "-"}
<p> </p>
<strong>Id:</strong> {patientDetails.id ?? "-"} </div>
</p> )}
</div> </AlertDialogDescription>
)} </AlertDialogHeader>
</AlertDialogDescription> <AlertDialogFooter>
</AlertDialogHeader> <AlertDialogCancel>Fechar</AlertDialogCancel>
<AlertDialogFooter> </AlertDialogFooter>
<AlertDialogCancel>Fechar</AlertDialogCancel> </AlertDialogContent>
</AlertDialogFooter> </AlertDialog>
</AlertDialogContent> </div>
</AlertDialog> </ManagerLayout>
</div> );
</ManagerLayout>
);
} }

View File

@ -6,239 +6,412 @@ import ManagerLayout from "@/components/manager-layout";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Plus, Eye, Filter, Loader2 } from "lucide-react"; import { Plus, Eye, Filter, Loader2 } from "lucide-react";
import { import {
AlertDialog, AlertDialog,
AlertDialogCancel, AlertDialogCancel,
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { api, login } from "services/api.mjs"; import { api, login } from "services/api.mjs";
import { usersService } from "services/usersApi.mjs"; import { usersService } from "services/usersApi.mjs";
interface FlatUser { interface FlatUser {
id: string; id: string;
user_id: string; user_id: string;
full_name?: string; full_name?: string;
email: string; email: string;
phone?: string | null; phone?: string | null;
role: string; role: string;
} }
interface UserInfoResponse { interface UserInfoResponse {
user: any; user: any;
profile: any; profile: any;
roles: string[]; roles: string[];
permissions: Record<string, boolean>; permissions: Record<string, boolean>;
} }
export default function UsersPage() { export default function UsersPage() {
const [users, setUsers] = useState<FlatUser[]>([]); const [users, setUsers] = useState<FlatUser[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(null); const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(
const [selectedRole, setSelectedRole] = useState<string>(""); 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");
const fetchUsers = useCallback(async () => { // --- Lógica de Paginação INÍCIO ---
setLoading(true); const [itemsPerPage, setItemsPerPage] = useState(10);
setError(null); const [currentPage, setCurrentPage] = useState(1);
try {
// 1) pega roles
const rolesData: any[] = await usersService.list_roles();
// Garante que rolesData é array
const rolesArray = Array.isArray(rolesData) ? rolesData : [];
// 2) pega todos os profiles de uma vez (para evitar muitos requests) // Lógica para mudar itens por página, resetando para a página 1
const profilesData: any[] = await api.get(`/rest/v1/profiles?select=id,full_name,email,phone`); const handleItemsPerPageChange = (value: string) => {
const profilesById = new Map<string, any>(); setItemsPerPage(Number(value));
if (Array.isArray(profilesData)) { setCurrentPage(1); // Resetar para a primeira página
for (const p of profilesData) {
if (p?.id) profilesById.set(p.id, p);
}
}
// 3) mapear roles -> flat users, usando ID específico de cada item
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
const uid = roleItem.user_id;
const profile = profilesById.get(uid);
return {
id: uid,
user_id: uid,
full_name: profile?.full_name ?? "—",
email: profile?.email ?? "—",
phone: profile?.phone ?? "—",
role: roleItem.role ?? "—",
};
});
setUsers(mapped);
console.log("[fetchUsers] mapped count:", mapped.length);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Veja console.");
setUsers([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
const init = async () => {
try {
await login(); // garante token
} catch (e) {
console.warn("login falhou no init:", e);
}
await fetchUsers();
}; };
init(); // --- Lógica de Paginação FIM ---
}, [fetchUsers]);
const openDetailsDialog = async (flatUser: FlatUser) => { const fetchUsers = useCallback(async () => {
setDetailsDialogOpen(true); setLoading(true);
setUserDetails(null); setError(null);
try {
const rolesData: any[] = await usersService.list_roles();
const rolesArray = Array.isArray(rolesData) ? rolesData : [];
try { const profilesData: any[] = await api.get(
console.log("[openDetailsDialog] user_id:", flatUser.user_id); `/rest/v1/profiles?select=id,full_name,email,phone`
const data = await usersService.full_data(flatUser.user_id); );
console.log("[openDetailsDialog] full_data returned:", data);
setUserDetails(data);
} catch (err: any) {
console.error("Erro ao carregar detalhes:", err);
// fallback com dados já conhecidos
setUserDetails({
user: { id: flatUser.user_id, email: flatUser.email },
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
roles: [flatUser.role],
permissions: {},
});
}
};
const filteredUsers = const profilesById = new Map<string, any>();
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users; if (Array.isArray(profilesData)) {
for (const p of profilesData) {
if (p?.id) profilesById.set(p.id, p);
}
}
return ( const mapped: FlatUser[] = rolesArray.map((roleItem) => {
<ManagerLayout> const uid = roleItem.user_id;
<div className="space-y-6"> const profile = profilesById.get(uid);
<div className="flex items-center justify-between"> return {
<div> id: uid,
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1> user_id: uid,
<p className="text-sm text-gray-500">Gerencie usuários.</p> full_name: profile?.full_name ?? "—",
</div> email: profile?.email ?? "—",
<Link href="/manager/usuario/novo"> phone: profile?.phone ?? "—",
<Button className="bg-green-600 hover:bg-green-700"> role: roleItem.role ?? "—",
<Plus className="w-4 h-4 mr-2" /> Novo Usuário };
</Button> });
</Link>
</div>
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200"> setUsers(mapped);
<Filter className="w-5 h-5 text-gray-400" /> setCurrentPage(1); // Resetar a página após carregar
<Select onValueChange={setSelectedRole} value={selectedRole}> console.log("[fetchUsers] mapped count:", mapped.length);
<SelectTrigger className="w-[180px]"> } catch (err: any) {
<SelectValue placeholder="Filtrar por papel" /> console.error("Erro ao buscar usuários:", err);
</SelectTrigger> setError("Não foi possível carregar os usuários. Veja console.");
<SelectContent> setUsers([]);
<SelectItem value="all">Todos</SelectItem> } finally {
<SelectItem value="admin">Admin</SelectItem> setLoading(false);
<SelectItem value="gestor">Gestor</SelectItem> }
<SelectItem value="medico">Médico</SelectItem> }, []);
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden"> useEffect(() => {
{loading ? ( const init = async () => {
<div className="p-8 text-center text-gray-500"> try {
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" /> await login();
Carregando usuários... } catch (e) {
</div> console.warn("login falhou no init:", e);
) : error ? ( }
<div className="p-8 text-center text-red-600">{error}</div> await fetchUsers();
) : filteredUsers.length === 0 ? ( };
<div className="p-8 text-center text-gray-500"> init();
Nenhum usuário encontrado. }, [fetchUsers]);
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<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>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm text-gray-500">{u.id}</td>
<td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.email}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td>
<td className="px-6 py-4 text-sm text-gray-500 capitalize">{u.role}</td>
<td className="px-6 py-4 text-right">
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar">
<Eye className="h-4 w-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> const openDetailsDialog = async (flatUser: FlatUser) => {
<AlertDialogContent> setDetailsDialogOpen(true);
<AlertDialogHeader> setUserDetails(null);
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
<AlertDialogDescription> try {
{!userDetails ? ( console.log("[openDetailsDialog] user_id:", flatUser.user_id);
<div className="p-4 text-center text-gray-500"> const data = await usersService.full_data(flatUser.user_id);
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" /> console.log("[openDetailsDialog] full_data returned:", data);
Buscando dados completos... setUserDetails(data);
</div> } catch (err: any) {
) : ( console.error("Erro ao carregar detalhes:", err);
<div className="space-y-3 pt-2 text-left text-gray-700"> setUserDetails({
<div><strong>ID:</strong> {userDetails.user.id}</div> user: { id: flatUser.user_id, email: flatUser.email },
<div><strong>E-mail:</strong> {userDetails.user.email}</div> profile: { full_name: flatUser.full_name, phone: flatUser.phone },
<div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div> roles: [flatUser.role],
<div><strong>Telefone:</strong> {userDetails.profile.phone}</div> permissions: {},
<div><strong>Roles:</strong> {userDetails.roles?.join(", ")}</div> });
}
};
// 1. Filtragem
const filteredUsers =
selectedRole && selectedRole !== "all"
? users.filter((u) => u.role === selectedRole)
: users;
// 2. Paginação (aplicada sobre a lista filtrada)
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentItems = filteredUsers.slice(indexOfFirstItem, indexOfLastItem);
// Função para mudar de página
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const totalPages = Math.ceil(filteredUsers.length / itemsPerPage);
// --- Funções e Lógica de Navegação ADICIONADAS ---
const goToPrevPage = () => {
setCurrentPage((prev) => Math.max(1, prev - 1));
};
const goToNextPage = () => {
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
// Lógica para gerar os números das páginas visíveis
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
const pages: number[] = [];
const maxVisiblePages = 5; // Número máximo de botões de página a serem exibidos (ex: 2, 3, 4, 5, 6)
const halfRange = Math.floor(maxVisiblePages / 2);
let startPage = Math.max(1, currentPage - halfRange);
let endPage = Math.min(totalPages, currentPage + halfRange);
// Ajusta para manter o número fixo de botões quando nos limites
if (endPage - startPage + 1 < maxVisiblePages) {
if (endPage === totalPages) {
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
if (startPage === 1) {
endPage = Math.min(totalPages, maxVisiblePages);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
// --- Fim das Funções e Lógica de Navegação ADICIONADAS ---
return (
<ManagerLayout>
<div className="space-y-6 px-2 sm:px-4 md:px-8">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3">
<div> <div>
<strong>Permissões:</strong> <h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
<ul className="list-disc list-inside"> <p className="text-sm text-gray-500">Gerencie usuários.</p>
{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: {v ? "Sim" : "Não"}</li>)}
</ul>
</div> </div>
</div> <Link href="/manager/usuario/novo" className="w-full sm:w-auto">
)} <Button className="w-full sm:w-auto bg-green-600 hover:bg-green-700">
</AlertDialogDescription> <Plus className="w-4 h-4 mr-2" /> Novo Usuário
</AlertDialogHeader> </Button>
<AlertDialogFooter> </Link>
<AlertDialogCancel>Fechar</AlertDialogCancel> </div>
</AlertDialogFooter>
</AlertDialogContent> {/* Filtro e Itens por Página */}
</AlertDialog> <div className="flex flex-wrap items-center gap-3 bg-white p-4 rounded-lg border border-gray-200">
</div> <Filter className="w-5 h-5 text-gray-400" />
</ManagerLayout>
); {/* Select de Filtro por Papel - Ajustado para resetar a página */}
<Select
onValueChange={(value) => {
setSelectedRole(value);
setCurrentPage(1); // Resetar para a primeira página ao mudar o filtro
}}
value={selectedRole}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar por papel" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
{/* Select de Itens por Página */}
<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>
{/* Fim do Filtro e Itens por Página */}
{/* Tabela */}
<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">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />
Carregando usuários...
</div>
) : error ? (
<div className="p-8 text-center text-red-600">{error}</div>
) : filteredUsers.length === 0 ? (
<div className="p-8 text-center text-gray-500">
Nenhum usuário encontrado com os filtros aplicados.
</div>
) : (
<>
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 hidden md:table-header-group">
<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>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
</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>
<td className="px-6 py-4 text-sm text-gray-900">
{u.full_name}
</td>
<td className="px-6 py-4 text-sm text-gray-500 break-all">
{u.email}
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{u.phone}
</td>
<td className="px-6 py-4 text-sm text-gray-500 capitalize">
{u.role}
</td>
<td className="px-6 py-4 text-right">
<Button
variant="outline"
size="icon"
onClick={() => openDetailsDialog(u)}
title="Visualizar"
>
<Eye className="h-4 w-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
{/* Paginação ATUALIZADA */}
{totalPages > 1 && (
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 border-t border-gray-200">
{/* 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
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>
))}
{/* 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-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300"
>
{"Próximo >"}
</button>
</div>
)}
{/* Fim da Paginação ATUALIZADA */}
</>
)}
</div>
{/* Modal de Detalhes */}
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-2xl">
{userDetails?.profile?.full_name || "Detalhes do Usuário"}
</AlertDialogTitle>
<AlertDialogDescription>
{!userDetails ? (
<div className="p-4 text-center text-gray-500">
<Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />
Buscando dados completos...
</div>
) : (
<div className="space-y-3 pt-2 text-left text-gray-700">
<div>
<strong>ID:</strong> {userDetails.user.id}
</div>
<div>
<strong>E-mail:</strong> {userDetails.user.email}
</div>
<div>
<strong>Nome completo:</strong>{" "}
{userDetails.profile.full_name}
</div>
<div>
<strong>Telefone:</strong> {userDetails.profile.phone}
</div>
<div>
<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">
{Object.entries(
userDetails.permissions || {}
).map(([k, v]) => (
<li key={k}>
{k}: <span className={`font-semibold ${v ? 'text-green-600' : 'text-red-600'}`}>{v ? "Sim" : "Não"}</span>
</li>
))}
</ul>
</div>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Fechar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</ManagerLayout>
);
} }

View File

@ -1,404 +1,466 @@
// app/secretary/pacientes/page.tsx
"use client"; "use client";
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useCallback } 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
DropdownMenu, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
DropdownMenuContent, import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
DropdownMenuItem, import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Edit, Trash2, Eye, Calendar, Filter } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import SecretaryLayout from "@/components/secretary-layout"; import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
// --- INÍCIO DA CORREÇÃO --- // Defina o tamanho da página.
interface Patient { const PAGE_SIZE = 5;
id: string;
nome: string;
telefone: string;
cidade: string;
estado: string;
ultimoAtendimento: string;
proximoAtendimento: string;
vip: boolean;
convenio: string;
status?: string;
// Propriedades detalhadas para o modal
full_name?: string;
cpf?: string;
email?: string;
phone_mobile?: string;
phone1?: string;
phone2?: string;
social_name?: string;
sex?: string;
blood_type?: string;
weight_kg?: number;
height_m?: number;
bmi?: number;
street?: string;
neighborhood?: string;
city?: string; // <-- Adicionado
state?: string; // <-- Adicionado
cep?: string;
created_at?: string;
updated_at?: string;
}
// --- FIM DA CORREÇÃO ---
// Função para formatar a data
const formatDate = (dateString: string | null | undefined): string => {
if (!dateString) {
return "N/A";
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return "Data inválida";
}
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
} catch (error) {
return "Data inválida";
}
};
export default function PacientesPage() { export default function PacientesPage() {
const [searchTerm, setSearchTerm] = useState(""); // --- ESTADOS DE DADOS E GERAL ---
const [convenioFilter, setConvenioFilter] = useState("all"); const [searchTerm, setSearchTerm] = useState("");
const [vipFilter, setVipFilter] = useState("all"); const [convenioFilter, setConvenioFilter] = useState("all");
const [patients, setPatients] = useState<Patient[]>([]); const [vipFilter, setVipFilter] = useState("all");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasNext, setHasNext] = useState(true);
const [isFetching, setIsFetching] = useState(false);
const observerRef = useRef<HTMLDivElement | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<Patient | { error: string } | null>(null);
const openDetailsDialog = async (patientId: string) => { // Lista completa, carregada da API uma única vez
setDetailsDialogOpen(true); const [allPatients, setAllPatients] = useState<any[]>([]);
setPatientDetails(null); // Lista após a aplicação dos filtros (base para a paginação)
try { const [filteredPatients, setFilteredPatients] = useState<any[]>([]);
const res = await patientsService.getById(patientId);
setPatientDetails(res[0]);
} catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
}
};
const fetchPacientes = useCallback( const [loading, setLoading] = useState(true);
async (pageToFetch: number) => { const [error, setError] = useState<string | null>(null);
if (isFetching || !hasNext) return;
setIsFetching(true);
setError(null);
try {
const res = await patientsService.list();
const mapped: Patient[] = res.map((p: any) => ({
id: String(p.id ?? ""),
nome: p.full_name ?? "",
telefone: p.phone_mobile ?? p.phone1 ?? "",
cidade: p.city ?? "",
estado: p.state ?? "",
ultimoAtendimento: p.last_visit_at ?? "",
proximoAtendimento: p.next_appointment_at ?? "",
vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "",
status: p.status ?? undefined,
}));
setPatients((prev) => { // --- ESTADOS DE PAGINAÇÃO ---
const all = [...prev, ...mapped]; const [page, setPage] = useState(1);
const unique = Array.from(new Map(all.map((p) => [p.id, p])).values());
return unique; // CÁLCULO DA PAGINAÇÃO
const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE);
const startIndex = (page - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE;
// Pacientes a serem exibidos na tabela (aplicando a paginação)
const currentPatients = filteredPatients.slice(startIndex, endIndex);
// --- ESTADOS DE DIALOGS ---
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null);
// --- FUNÇÕES DE LÓGICA ---
// 1. Função para carregar TODOS os pacientes da API
const fetchAllPacientes = useCallback(
async () => {
setLoading(true);
setError(null);
try {
// Como o backend retorna um array, chamamos sem paginação
const res = await patientsService.list();
const mapped = res.map((p: any) => ({
id: String(p.id ?? ""),
nome: p.full_name ?? "—",
telefone: p.phone_mobile ?? p.phone1 ?? "—",
cidade: p.city ?? "—",
estado: p.state ?? "—",
// Formate as datas se necessário, aqui usamos como string
ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—",
vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "Particular", // Define um valor padrão
status: p.status ?? undefined,
}));
setAllPatients(mapped);
} catch (e: any) {
console.error(e);
setError(e?.message || "Erro ao buscar pacientes");
} finally {
setLoading(false);
}
},
[]
);
// 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam)
useEffect(() => {
const filtered = allPatients.filter((patient) => {
// Filtro por termo de busca (Nome ou Telefone)
const matchesSearch =
patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.telefone?.includes(searchTerm);
// Filtro por Convênio
const matchesConvenio =
convenioFilter === "all" ||
patient.convenio === convenioFilter;
// Filtro por VIP
const matchesVip =
vipFilter === "all" ||
(vipFilter === "vip" && patient.vip) ||
(vipFilter === "regular" && !patient.vip);
return matchesSearch && matchesConvenio && matchesVip;
}); });
if (mapped.length === 0) { setFilteredPatients(filtered);
setHasNext(false); // Garante que a página atual seja válida após a filtragem
} else { setPage(1);
setPage((prev) => prev + 1); }, [allPatients, searchTerm, convenioFilter, vipFilter]);
// 3. Efeito inicial para buscar os pacientes
useEffect(() => {
fetchAllPacientes();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) ---
const openDetailsDialog = async (patientId: string) => {
setDetailsDialogOpen(true);
setPatientDetails(null);
try {
const res = await patientsService.getById(patientId);
setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item
} catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
} }
} catch (e: any) {
setError(e?.message || "Erro ao buscar pacientes");
} finally {
setIsFetching(false);
}
},
[isFetching, hasNext]
);
useEffect(() => {
fetchPacientes(1);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!observerRef.current || !hasNext) return;
const observer = new window.IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isFetching && hasNext) {
fetchPacientes(page);
}
});
observer.observe(observerRef.current);
return () => {
if (observerRef.current) {
observer.unobserve(observerRef.current);
}
}; };
}, [fetchPacientes, page, hasNext, isFetching]);
const handleDeletePatient = async (patientId: string) => { const handleDeletePatient = async (patientId: string) => {
try { try {
await patientsService.delete(patientId); await patientsService.delete(patientId);
setPatients((prev) => prev.filter((p) => p.id !== patientId)); // Atualiza a lista completa para refletir a exclusão
} catch (e: any) { setAllPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId)));
setError(e?.message || "Erro ao deletar paciente"); } catch (e: any) {
alert("Erro ao deletar paciente."); alert(`Erro ao deletar paciente: ${e?.message || 'Erro desconhecido'}`);
} }
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setPatientToDelete(null); setPatientToDelete(null);
}; };
const openDeleteDialog = (patientId: string) => { const openDeleteDialog = (patientId: string) => {
setPatientToDelete(patientId); setPatientToDelete(patientId);
setDeleteDialogOpen(true); setDeleteDialogOpen(true);
}; };
const filteredPatients = patients.filter((patient) => { return (
const matchesSearch = <SecretaryLayout>
patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || <div className="space-y-6 px-2 sm:px-4 md:px-8">
patient.telefone?.includes(searchTerm); {/* Header (Responsividade OK) */}
const matchesConvenio = <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
convenioFilter === "all" || (patient.convenio ?? "") === convenioFilter; <div>
const matchesVip = <h1 className="text-xl md:text-2xl font-bold text-foreground">Pacientes</h1>
vipFilter === "all" || <p className="text-muted-foreground text-sm md:text-base">Gerencie as informações de seus pacientes</p>
(vipFilter === "vip" && patient.vip) || </div>
(vipFilter === "regular" && !patient.vip); <div className="flex gap-2">
<Link href="/secretary/pacientes/novo" className="w-full md:w-auto">
<Button className="w-full bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
Adicionar
</Button>
</Link>
</div>
</div>
return matchesSearch && matchesConvenio && matchesVip; {/* Bloco de Filtros (Responsividade APLICADA) */}
}); <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" />
return ( {/* Busca - Ocupa 100% no mobile, depois cresce */}
<SecretaryLayout> <input
<div className="space-y-6"> type="text"
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> placeholder="Buscar por nome ou telefone..."
<div> value={searchTerm}
<h1 className="text-xl md:text-2xl font-bold text-foreground">Pacientes</h1> onChange={(e) => setSearchTerm(e.target.value)}
<p className="text-muted-foreground text-sm md:text-base">Gerencie as informações de seus pacientes</p> className="w-full sm:flex-grow sm:min-w-[150px] p-2 border rounded-md text-sm"
</div> />
<div className="flex gap-2">
<Link href="/secretary/pacientes/novo">
<Button className="w-full md:w-auto">
<Plus className="w-4 h-4 mr-2" />
Adicionar
</Button>
</Link>
</div>
</div>
<div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border"> {/* Convênio - Ocupa metade da linha no mobile */}
<div className="flex items-center gap-2 w-full md:w-auto"> <div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[200px]">
<span className="text-sm font-medium text-foreground">Convênio</span> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">Convênio</span>
<Select value={convenioFilter} onValueChange={setConvenioFilter}> <Select value={convenioFilter} onValueChange={setConvenioFilter}>
<SelectTrigger className="w-full md:w-40"> <SelectTrigger className="w-full sm:w-40">
<SelectValue placeholder="Selecione o Convênio" /> <SelectValue placeholder="Convênio" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">Todos</SelectItem> <SelectItem value="all">Todos</SelectItem>
<SelectItem value="Particular">Particular</SelectItem> <SelectItem value="Particular">Particular</SelectItem>
<SelectItem value="SUS">SUS</SelectItem> <SelectItem value="SUS">SUS</SelectItem>
<SelectItem value="Unimed">Unimed</SelectItem> <SelectItem value="Unimed">Unimed</SelectItem>
</SelectContent> {/* Adicione outros convênios conforme necessário */}
</Select> </SelectContent>
</div> </Select>
<div className="flex items-center gap-2 w-full md:w-auto"> </div>
<span className="text-sm font-medium text-foreground">VIP</span>
<Select value={vipFilter} onValueChange={setVipFilter}>
<SelectTrigger className="w-full md:w-32">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="vip">VIP</SelectItem>
<SelectItem value="regular">Regular</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 w-full md:w-auto">
<span className="text-sm font-medium text-foreground">Aniversariantes</span>
<Select>
<SelectTrigger className="w-full md:w-32">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="today">Hoje</SelectItem>
<SelectItem value="week">Esta semana</SelectItem>
<SelectItem value="month">Este mês</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 className="bg-white rounded-lg border border-gray-200"> {/* VIP - Ocupa a outra metade da linha no mobile */}
<div className="overflow-x-auto"> <div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[150px]">
{error && <div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>} <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
<table className="w-full min-w-[600px]"> <Select value={vipFilter} onValueChange={setVipFilter}>
<thead className="bg-gray-50 border-b border-gray-200"> <SelectTrigger className="w-full sm:w-32">
<tr> <SelectValue placeholder="VIP" />
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Nome</th> </SelectTrigger>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Telefone</th> <SelectContent>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Cidade</th> <SelectItem value="all">Todos</SelectItem>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Estado</th> <SelectItem value="vip">VIP</SelectItem>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Último atendimento</th> <SelectItem value="regular">Regular</SelectItem>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Próximo atendimento</th> </SelectContent>
<th className="text-left p-2 md:p-4 font-medium text-gray-700">Ações</th> </Select>
</tr> </div>
</thead>
<tbody> {/* Aniversariantes - Vai para a linha de baixo no mobile, ocupando 100% */}
{filteredPatients.length === 0 && !isFetching ? ( <Button variant="outline" className="w-full md:w-auto md:ml-auto">
<tr> <Calendar className="w-4 h-4 mr-2" />
<td colSpan={7} className="p-8 text-center text-gray-500"> Aniversariantes
{patients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} </Button>
</td> </div>
</tr>
) : ( {/* Tabela (Responsividade APLICADA) */}
filteredPatients.map((patient) => ( <div className="bg-white rounded-lg border border-gray-200 shadow-md">
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50"> <div className="overflow-x-auto">
<td className="p-4"> {error ? (
<div className="flex items-center gap-3"> <div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center"> ) : loading ? (
<span className="text-gray-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span> <div className="p-6 text-center text-gray-500 flex items-center justify-center">
</div> <Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes...
<span className="font-medium text-gray-900">{patient.nome}</span> </div>
) : (
// min-w ajustado para responsividade
<table className="w-full min-w-[650px] md:min-w-[900px]">
<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 */}
<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>
</tr>
</thead>
<tbody>
{currentPatients.length === 0 ? (
<tr>
<td colSpan={7} className="p-8 text-center text-gray-500">
{allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"}
</td>
</tr>
) : (
currentPatients.map((patient) => (
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-green-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span>
</div>
<span className="font-medium text-gray-900">
{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>
)}
</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>
<td className="p-4 text-gray-600 hidden lg:table-cell">{patient.ultimoAtendimento}</td>
<td className="p-4 text-gray-600 hidden lg:table-cell">{patient.proximoAtendimento}</td>
<td className="p-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-blue-600 cursor-pointer">Ações</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<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>
<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>
</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-wrap items-center justify-between p-4 border-t border-gray-200">
{/* Adicionado contador de página para melhor informação no mobile */}
{/* Botões de Navegação */}
<div className="flex space-x-1 order-1 w-full justify-center sm:w-auto sm:justify-start sm:order-2">
{/* Botão Anterior */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="sm"
>
&lt; Anterior
</Button>
{/* Renderização dos botões de número de página (Limitando a 5) */}
{Array.from({ length: totalPages }, (_, index) => index + 1)
// Limita a exibição dos botões para 5 (janela de visualização)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="sm"
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
{/* Botão Próximo */}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="sm"
>
Próximo &gt;
</Button>
</div>
</div> </div>
</td> )}
<td className="p-4 text-gray-600">{patient.telefone}</td> </div>
<td className="p-4 text-gray-600">{patient.cidade}</td>
<td className="p-4 text-gray-600">{patient.estado}</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">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="text-blue-600 hover:bg-blue-50 hover:text-blue-700 h-8 px-2">Ações</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(patient.id)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/secretary/pacientes/${patient.id}/editar`}>
<Edit className="w-4 h-4 mr-2" />
Editar
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600 focus:text-red-600" onClick={() => openDeleteDialog(patient.id)}>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
<div ref={observerRef} style={{ height: 1 }} />
{isFetching && <div className="p-4 text-center text-gray-500">Carregando mais pacientes...</div>}
</div>
</div>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> {/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialogContent> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogHeader> {/* ... (AlertDialog de Exclusão) ... */}
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle> <AlertDialogContent>
<AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription> <AlertDialogHeader>
</AlertDialogHeader> <AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogFooter> <AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription>
<AlertDialogCancel>Cancelar</AlertDialogCancel> </AlertDialogHeader>
<AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> <AlertDialogFooter>
Excluir <AlertDialogCancel>Cancelar</AlertDialogCancel>
</AlertDialogAction> <AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700">
</AlertDialogFooter> Excluir
</AlertDialogContent> </AlertDialogAction>
</AlertDialog> </AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent> {/* ... (AlertDialog de Detalhes) ... */}
<AlertDialogHeader> <AlertDialogContent>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle> <AlertDialogHeader>
{patientDetails === null ? ( <AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
<div className="text-gray-500 text-center py-4">Carregando...</div> <AlertDialogDescription>
) : 'error' in patientDetails ? ( {patientDetails === null ? (
<div className="text-red-600 text-center py-4">{patientDetails.error}</div> <div className="text-gray-500">
) : ( <Loader2 className="w-6 h-6 animate-spin mx-auto text-green-600 my-4" />
<AlertDialogDescription asChild> Carregando...
<div className="space-y-2 text-left max-h-[60vh] overflow-y-auto pr-2"> </div>
<p><strong>Nome:</strong> {patientDetails.full_name}</p> ) : patientDetails?.error ? (
<p><strong>CPF:</strong> {patientDetails.cpf || "-"}</p> <div className="text-red-600 p-4">{patientDetails.error}</div>
<p><strong>Email:</strong> {patientDetails.email || "-"}</p> ) : (
<p><strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}</p> <div className="grid gap-4 py-4">
<p><strong>Nome social:</strong> {patientDetails.social_name ?? "-"}</p> <div className="grid grid-cols-2 gap-4">
<p><strong>Sexo:</strong> {patientDetails.sex ?? "-"}</p> <div>
<p><strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"}</p> <p className="font-semibold">Nome Completo</p>
<p><strong>Peso:</strong> {patientDetails.weight_kg ? `${patientDetails.weight_kg}kg` : "-"}</p> <p>{patientDetails.full_name}</p>
<p><strong>Altura:</strong> {patientDetails.height_m ? `${patientDetails.height_m}m` : "-"}</p> </div>
<p><strong>IMC:</strong> {patientDetails.bmi ?? "-"}</p> <div>
<p><strong>Endereço:</strong> {patientDetails.street ?? "-"}</p> <p className="font-semibold">Email</p>
<p><strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"}</p> <p>{patientDetails.email}</p>
<p><strong>Cidade:</strong> {patientDetails.city ?? "-"}</p> </div>
<p><strong>Estado:</strong> {patientDetails.state ?? "-"}</p> <div>
<p><strong>CEP:</strong> {patientDetails.cep ?? "-"}</p> <p className="font-semibold">Telefone</p>
<p><strong>Criado em:</strong> {formatDate(patientDetails.created_at)}</p> <p>{patientDetails.phone_mobile}</p>
<p><strong>Atualizado em:</strong> {formatDate(patientDetails.updated_at)}</p> </div>
<p><strong>Id:</strong> {patientDetails.id ?? "-"}</p> <div>
</div> <p className="font-semibold">Data de Nascimento</p>
</AlertDialogDescription> <p>{patientDetails.birth_date}</p>
)} </div>
</AlertDialogHeader> <div>
<AlertDialogFooter> <p className="font-semibold">CPF</p>
<AlertDialogCancel>Fechar</AlertDialogCancel> <p>{patientDetails.cpf}</p>
</AlertDialogFooter> </div>
</AlertDialogContent> <div>
</AlertDialog> <p className="font-semibold">Tipo Sanguíneo</p>
</div> <p>{patientDetails.blood_type}</p>
</SecretaryLayout> </div>
); <div>
<p className="font-semibold">Peso (kg)</p>
<p>{patientDetails.weight_kg}</p>
</div>
<div>
<p className="font-semibold">Altura (m)</p>
<p>{patientDetails.height_m}</p>
</div>
</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>
<p className="font-semibold">Rua</p>
<p>{`${patientDetails.street}, ${patientDetails.number}`}</p>
</div>
<div>
<p className="font-semibold">Complemento</p>
<p>{patientDetails.complement}</p>
</div>
<div>
<p className="font-semibold">Bairro</p>
<p>{patientDetails.neighborhood}</p>
</div>
<div>
<p className="font-semibold">Cidade</p>
<p>{patientDetails.cidade}</p>
</div>
<div>
<p className="font-semibold">Estado</p>
<p>{patientDetails.estado}</p>
</div>
<div>
<p className="font-semibold">CEP</p>
<p>{patientDetails.cep}</p>
</div>
</div>
</div>
</div>
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Fechar</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</SecretaryLayout>
);
} }