Merge pull request #28 from m1guelmcf/updt-report

J.R.
This commit is contained in:
DaniloSts 2025-11-27 09:24:56 -03:00 committed by GitHub
commit 248e90595e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 450 additions and 789 deletions

View File

@ -2,12 +2,7 @@
import { useEffect, useState, useCallback } from "react"; import { useEffect, useState, useCallback } from "react";
import Link from "next/link"; import Link from "next/link";
import { import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Eye, Edit, Calendar, Trash2, Loader2, MoreVertical } from "lucide-react"; import { Eye, Edit, Calendar, Trash2, Loader2, MoreVertical } 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";
@ -200,210 +195,181 @@ export default function PacientesPage() {
</div> </div>
</div> </div>
<div className="bg-card rounded-lg border border-border overflow-hidden shadow-md"> <div className="bg-card rounded-lg border border-border overflow-hidden shadow-md">
{/* Tabela para Telas Médias e Grandes */} {/* Tabela para Telas Médias e Grandes */}
<div className="overflow-x-auto hidden md:block"> <div className="overflow-x-auto hidden md:block"> {/* Esconde em telas pequenas */}
{" "} <table className="min-w-[600px] w-full">
{/* Esconde em telas pequenas */} <thead className="bg-muted border-b border-border">
<table className="min-w-[600px] w-full"> <tr>
<thead className="bg-muted border-b border-border"> <th className="text-left p-3 sm:p-4 font-medium text-foreground">Nome</th>
<tr> <th className="text-left p-3 sm:p-4 font-medium text-foreground">
<th className="text-left p-3 sm:p-4 font-medium text-foreground"> Telefone
Nome </th>
</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
<th className="text-left p-3 sm:p-4 font-medium text-foreground"> Cidade
Telefone </th>
</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell">
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell"> Estado
Cidade </th>
</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden lg:table-cell"> Último atendimento
Estado </th>
</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell"> Próximo atendimento
Último atendimento </th>
</th> <th className="text-left p-3 sm:p-4 font-medium text-foreground">Ações</th>
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell"> </tr>
Próximo atendimento </thead>
</th> <tbody>
<th className="text-left p-3 sm:p-4 font-medium text-foreground"> {loading ? (
Ações <tr>
</th> <td colSpan={7} className="p-6 text-muted-foreground text-center">
</tr> <Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
</thead> Carregando pacientes...
<tbody> </td>
{loading ? ( </tr>
<tr> ) : error ? (
<td <tr>
colSpan={7} <td colSpan={7} className="p-6 text-red-600 text-center">{`Erro: ${error}`}</td>
className="p-6 text-muted-foreground text-center" </tr>
> ) : pacientes.length === 0 ? (
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" /> <tr>
Carregando pacientes... <td colSpan={7} className="p-8 text-center text-muted-foreground">
</td> Nenhum paciente encontrado
</tr> </td>
) : error ? ( </tr>
<tr> ) : (
<td currentItems.map((p) => (
colSpan={7} <tr
className="p-6 text-red-600 text-center" key={p.id}
>{`Erro: ${error}`}</td> className="border-b border-border hover:bg-accent/40 transition-colors"
</tr> >
) : pacientes.length === 0 ? ( <td className="p-3 sm:p-4">{p.nome}</td>
<tr> <td className="p-3 sm:p-4 text-muted-foreground">
<td {p.telefone}
colSpan={7} </td>
className="p-8 text-center text-muted-foreground" <td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
> {p.cidade}
Nenhum paciente encontrado </td>
</td> <td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell">
</tr> {p.estado}
) : ( </td>
currentItems.map((p) => ( <td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
<tr {p.ultimoAtendimento}
key={p.id} </td>
className="border-b border-border hover:bg-accent/40 transition-colors" <td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell">
> {p.proximoAtendimento}
<td className="p-3 sm:p-4">{p.nome}</td> </td>
<td className="p-3 sm:p-4 text-muted-foreground"> <td className="p-3 sm:p-4">
{p.telefone} <DropdownMenu>
</td> <DropdownMenuTrigger asChild>
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell"> <Button variant="ghost" className="h-8 w-8 p-0">
{p.cidade} <span className="sr-only">Abrir menu</span>
</td> <MoreVertical className="h-4 w-4" />
<td className="p-3 sm:p-4 text-muted-foreground hidden lg:table-cell"> </Button>
{p.estado} </DropdownMenuTrigger>
</td> <DropdownMenuContent align="end">
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell"> <DropdownMenuItem onClick={() => handleOpenModal(p)}>
{p.ultimoAtendimento} <Eye className="w-4 h-4 mr-2" />
</td> Ver detalhes
<td className="p-3 sm:p-4 text-muted-foreground hidden xl:table-cell"> </DropdownMenuItem>
{p.proximoAtendimento} <DropdownMenuItem asChild>
</td> <Link href={`/doctor/medicos/${p.id}/laudos`}>
<td className="p-3 sm:p-4"> <Edit className="w-4 h-4 mr-2" />
<DropdownMenu> Laudos
<DropdownMenuTrigger asChild> </Link>
<Button variant="ghost" className="h-8 w-8 p-0"> </DropdownMenuItem>
<span className="sr-only">Abrir menu</span> {/* <DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
<MoreVertical className="h-4 w-4" /> <Calendar className="w-4 h-4 mr-2" />
</Button> Ver agenda
</DropdownMenuTrigger> </DropdownMenuItem> */}
<DropdownMenuContent align="end"> {/* <DropdownMenuItem
<DropdownMenuItem onClick={() => {
onClick={() => handleOpenModal(p)} const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
> setPacientes(newPacientes);
<Eye className="w-4 h-4 mr-2" /> alert(`Paciente ID: ${p.id} excluído`);
Ver detalhes }}
</DropdownMenuItem> className="text-red-600 focus:bg-red-50 focus:text-red-600"
<DropdownMenuItem asChild> >
<Link href={`/doctor/medicos/${p.id}/laudos`}> <Trash2 className="w-4 h-4 mr-2" />
<Edit className="w-4 h-4 mr-2" /> Excluir
Laudos </DropdownMenuItem> */}
</Link> </DropdownMenuContent>
</DropdownMenuItem> </DropdownMenu>
<DropdownMenuItem </td>
onClick={() => { </tr>
const newPacientes = pacientes.filter( ))
(pac) => pac.id !== p.id )}
); </tbody>
setPacientes(newPacientes); </table>
alert(`Paciente ID: ${p.id} excluído`); </div>
}}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Layout em Cards/Lista para Telas Pequenas */} {/* Layout em Cards/Lista para Telas Pequenas */}
<div className="md:hidden divide-y divide-border"> <div className="md:hidden divide-y divide-border"> {/* Visível apenas em telas pequenas */}
{" "} {loading ? (
{/* Visível apenas em telas pequenas */} <div className="p-6 text-muted-foreground text-center">
{loading ? ( <Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
<div className="p-6 text-muted-foreground text-center"> Carregando pacientes...
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" /> </div>
Carregando pacientes... ) : error ? (
</div> <div className="p-6 text-red-600 text-center">{`Erro: ${error}`}</div>
) : error ? ( ) : pacientes.length === 0 ? (
<div className="p-6 text-red-600 text-center">{`Erro: ${error}`}</div> <div className="p-8 text-center text-muted-foreground">
) : pacientes.length === 0 ? ( Nenhum paciente encontrado
<div className="p-8 text-center text-muted-foreground"> </div>
Nenhum paciente encontrado ) : (
</div> currentItems.map((p) => (
) : ( <div key={p.id} className="flex items-center justify-between p-4 hover:bg-accent/40 transition-colors">
currentItems.map((p) => ( <div className="flex-1 min-w-0 pr-4"> {/* Adicionado padding à direita */}
<div <div className="text-base font-semibold text-foreground break-words"> {/* Aumentado a fonte e break-words para evitar corte do nome */}
key={p.id} {p.nome || "—"}
className="flex items-center justify-between p-4 hover:bg-accent/40 transition-colors" </div>
> {/* Removido o 'truncate' e adicionado 'break-words' no telefone */}
<div className="flex-1 min-w-0 pr-4"> <div className="text-sm text-muted-foreground break-words">
{" "} Telefone: **{p.telefone || "N/A"}**
{/* Adicionado padding à direita */} </div>
<div className="text-base font-semibold text-foreground break-words"> </div>
{" "} <div className="ml-4 flex-shrink-0">
{/* Aumentado a fonte e break-words para evitar corte do nome */} <DropdownMenu>
{p.nome || "—"} <DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Eye className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" />
Laudos
</Link>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
<Calendar className="w-4 h-4 mr-2" />
Ver agenda
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
setPacientes(newPacientes);
alert(`Paciente ID: ${p.id} excluído`);
}}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))
)}
</div> </div>
{/* Removido o 'truncate' e adicionado 'break-words' no telefone */}
<div className="text-sm text-muted-foreground break-words">
Telefone: **{p.telefone || "N/A"}**
</div>
</div>
<div className="ml-4 flex-shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Eye className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/doctor/pacientes/${p.id}/laudos`}>
<Edit className="w-4 h-4 mr-2" />
Laudos
</Link>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
alert(`Agenda para paciente ID: ${p.id}`)
}
>
<Calendar className="w-4 h-4 mr-2" />
Ver agenda
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
const newPacientes = pacientes.filter(
(pac) => pac.id !== p.id
);
setPacientes(newPacientes);
alert(`Paciente ID: ${p.id} excluído`);
}}
className="text-red-600 focus:bg-red-50 focus:text-red-600"
>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))
)}
</div>
{/* Paginação */} {/* Paginação */}
{totalPages > 1 && ( {totalPages > 1 && (
@ -445,11 +411,11 @@ export default function PacientesPage() {
</div> </div>
</div> </div>
<PatientDetailsModal <PatientDetailsModal
patient={selectedPatient} patient={selectedPatient}
isOpen={isModalOpen} isOpen={isModalOpen}
onClose={handleCloseModal} onClose={handleCloseModal}
/> />
</Sidebar> </Sidebar>
); );
} }

View File

@ -3,14 +3,13 @@
import React, { useEffect, useState, useCallback, useMemo } from "react"; import React, { useEffect, useState, useCallback, useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Edit, Trash2, Eye, Calendar, Loader2, MoreVertical } from "lucide-react"; import { Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react"
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"
// Imports dos Serviços import { doctorsService } from "services/doctorsApi.mjs";
import { doctorsService } from "@/services/doctorsApi.mjs";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
// --- NOVOS IMPORTS (Certifique-se que criou os arquivos no passo anterior) --- // --- NOVOS IMPORTS (Certifique-se que criou os arquivos no passo anterior) ---
@ -326,9 +325,10 @@ export default function DoctorsPage() {
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className="text-black-600 cursor-pointer inline-block"> <Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Abrir menu</span>
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</div> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(doctor)}> <DropdownMenuItem onClick={() => openDetailsDialog(doctor)}>

View File

@ -3,39 +3,10 @@
import { useState, useEffect, 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 { Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } 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,
Loader2,
MoreVertical,
} from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
@ -43,60 +14,59 @@ import Sidebar from "@/components/Sidebar";
const PAGE_SIZE = 5; const PAGE_SIZE = 5;
export default function PacientesPage() { export default function PacientesPage() {
// --- ESTADOS DE DADOS E GERAL --- // --- ESTADOS DE DADOS E GERAL ---
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [convenioFilter, setConvenioFilter] = useState("all"); const [convenioFilter, setConvenioFilter] = useState("all");
const [vipFilter, setVipFilter] = useState("all"); const [vipFilter, setVipFilter] = useState("all");
// Lista completa, carregada da API uma única vez // Lista completa, carregada da API uma única vez
const [allPatients, setAllPatients] = useState<any[]>([]); const [allPatients, setAllPatients] = useState<any[]>([]);
// Lista após a aplicação dos filtros (base para a paginação) // Lista após a aplicação dos filtros (base para a paginação)
const [filteredPatients, setFilteredPatients] = useState<any[]>([]); const [filteredPatients, setFilteredPatients] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// --- ESTADOS DE PAGINAÇÃO --- // --- ESTADOS DE PAGINAÇÃO ---
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
// CÁLCULO DA PAGINAÇÃO // CÁLCULO DA PAGINAÇÃO
const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE); const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE);
const startIndex = (page - 1) * PAGE_SIZE; const startIndex = (page - 1) * PAGE_SIZE;
const endIndex = startIndex + PAGE_SIZE; const endIndex = startIndex + PAGE_SIZE;
// Pacientes a serem exibidos na tabela (aplicando a paginação) // Pacientes a serem exibidos na tabela (aplicando a paginação)
const currentPatients = filteredPatients.slice(startIndex, endIndex); const currentPatients = filteredPatients.slice(startIndex, endIndex);
// --- ESTADOS DE DIALOGS --- // --- ESTADOS DE DIALOGS ---
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null); const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null); const [patientDetails, setPatientDetails] = useState<any | null>(null);
// --- FUNÇÕES DE LÓGICA --- // --- FUNÇÕES DE LÓGICA ---
// 1. Função para carregar TODOS os pacientes da API // 1. Função para carregar TODOS os pacientes da API
const fetchAllPacientes = useCallback(async () => { const fetchAllPacientes = useCallback(
setLoading(true); async () => {
setError(null); setLoading(true);
try { setError(null);
// Como o backend retorna um array, chamamos sem paginação try {
const res = await patientsService.list(); // Como o backend retorna um array, chamamos sem paginação
const res = await patientsService.list();
const mapped = res.map((p: any) => ({ const mapped = res.map((p: any) => ({
id: String(p.id ?? ""), id: String(p.id ?? ""),
nome: p.full_name ?? "—", nome: p.full_name ?? "—",
telefone: p.phone_mobile ?? p.phone1 ?? "—", telefone: p.phone_mobile ?? p.phone1 ?? "—",
cidade: p.city ?? "—", cidade: p.city ?? "—",
estado: p.state ?? "—", estado: p.state ?? "—",
// Formate as datas se necessário, aqui usamos como string // Formate as datas se necessário, aqui usamos como string
ultimoAtendimento: p.last_visit_at?.split("T")[0] ?? "—", ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
proximoAtendimento: p.next_appointment_at proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—",
? p.next_appointment_at.split("T")[0].split("-").reverse().join("-") vip: Boolean(p.vip ?? false),
: "—", convenio: p.convenio ?? "Particular", // Define um valor padrão
vip: Boolean(p.vip ?? false), status: p.status ?? undefined,
convenio: p.convenio ?? "Particular", // Define um valor padrão }));
status: p.status ?? undefined,
}));
setAllPatients(mapped); setAllPatients(mapped);
} catch (e: any) { } catch (e: any) {
@ -107,31 +77,32 @@ export default function PacientesPage() {
} }
}, []); }, []);
// 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam) // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam)
useEffect(() => { useEffect(() => {
const filtered = allPatients.filter((patient) => { const filtered = allPatients.filter((patient) => {
// Filtro por termo de busca (Nome ou Telefone) // Filtro por termo de busca (Nome ou Telefone)
const matchesSearch = const matchesSearch =
patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.telefone?.includes(searchTerm); patient.telefone?.includes(searchTerm);
// Filtro por Convênio // Filtro por Convênio
const matchesConvenio = const matchesConvenio =
convenioFilter === "all" || patient.convenio === convenioFilter; convenioFilter === "all" ||
patient.convenio === convenioFilter;
// Filtro por VIP // Filtro por VIP
const matchesVip = const matchesVip =
vipFilter === "all" || vipFilter === "all" ||
(vipFilter === "vip" && patient.vip) || (vipFilter === "vip" && patient.vip) ||
(vipFilter === "regular" && !patient.vip); (vipFilter === "regular" && !patient.vip);
return matchesSearch && matchesConvenio && matchesVip; return matchesSearch && matchesConvenio && matchesVip;
}); });
setFilteredPatients(filtered); setFilteredPatients(filtered);
// Garante que a página atual seja válida após a filtragem // Garante que a página atual seja válida após a filtragem
setPage(1); setPage(1);
}, [allPatients, searchTerm, convenioFilter, vipFilter]); }, [allPatients, searchTerm, convenioFilter, vipFilter]);
// 3. Efeito inicial para buscar os pacientes // 3. Efeito inicial para buscar os pacientes
useEffect(() => { useEffect(() => {
@ -139,18 +110,18 @@ export default function PacientesPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) --- // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) ---
const openDetailsDialog = async (patientId: string) => { const openDetailsDialog = async (patientId: string) => {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setPatientDetails(null); setPatientDetails(null);
try { try {
const res = await patientsService.getById(patientId); const res = await patientsService.getById(patientId);
setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item
} catch (e: any) { } catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
} }
}; };
const handleDeletePatient = async (patientId: string) => { const handleDeletePatient = async (patientId: string) => {
try { try {
@ -186,20 +157,20 @@ export default function PacientesPage() {
</div> </div>
</div> </div>
{/* Bloco de Filtros (Responsividade APLICADA) */} {/* Bloco de Filtros (Responsividade APLICADA) */}
{/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */} {/* Adicionado flex-wrap para permitir que os itens quebrem para a linha de baixo */}
<div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border"> <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" /> <Filter className="w-5 h-5 text-gray-400" />
{/* Busca - Ocupa 100% no mobile, depois cresce */} {/* Busca - Ocupa 100% no mobile, depois cresce */}
<input <input
type="text" type="text"
placeholder="Buscar por nome ou telefone..." placeholder="Buscar por nome ou telefone..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
// w-full no mobile, depois flex-grow para ocupar o espaço disponível // w-full no mobile, depois flex-grow para ocupar o espaço disponível
className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm" className="w-full sm:flex-grow sm:max-w-[300px] p-2 border rounded-md text-sm"
/> />
{/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */} {/* Convênio - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]"> <div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[200px]">
@ -222,31 +193,22 @@ export default function PacientesPage() {
</Select> </Select>
</div> </div>
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */} {/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */}
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]"> <div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]">
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block"> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
VIP <Select value={vipFilter} onValueChange={setVipFilter}>
</span> <SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */}
<Select value={vipFilter} onValueChange={setVipFilter}> <SelectValue placeholder="VIP" />
<SelectTrigger className="w-full sm:w-32"> </SelectTrigger>
{" "} <SelectContent>
{/* w-full para mobile, w-32 para sm+ */} <SelectItem value="all">Todos</SelectItem>
<SelectValue placeholder="VIP" /> <SelectItem value="vip">VIP</SelectItem>
</SelectTrigger> <SelectItem value="regular">Regular</SelectItem>
<SelectContent> </SelectContent>
<SelectItem value="all">Todos</SelectItem> </Select>
<SelectItem value="vip">VIP</SelectItem> </div>
<SelectItem value="regular">Regular</SelectItem>
</SelectContent>
</Select>
</div>
{/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} </div>
<Button variant="outline" className="w-full md:w-auto md:ml-auto">
<Calendar className="w-4 h-4 mr-2" />
Aniversariantes
</Button>
</div>
{/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */} {/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */}
{/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */} {/* Garantir que a tabela se esconda em telas menores e apareça em MD+ */}

View File

@ -1,159 +1,67 @@
"use client" "use client"
import { useState, useEffect } from "react" import { useState, useEffect, useMemo } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/use-toast"
import { FileText, Download, Eye, Calendar, User, X } from "lucide-react" import { FileText, Download, Eye, Calendar, User, X, Loader2 } from "lucide-react"
import Sidebar from "@/components/Sidebar" import Sidebar from "@/components/Sidebar"
import { useAuthLayout } from "@/hooks/useAuthLayout"
import { reportsApi } from "@/services/reportsApi.mjs"
interface Report { interface Report {
id: string id: string;
title: string order_number: string;
doctor: string patient_id: string;
date: string status: string;
type: string exam: string;
status: "disponivel" | "pendente" requested_by: string;
description: string cid_code: string;
content: { diagnosis: string;
patientInfo: { conclusion: string;
name: string content_html: string;
age: number content_json: any;
gender: string hide_date: boolean;
id: string hide_signature: boolean;
} due_at: string;
examDetails: { created_by: string;
requestingDoctor: string updated_by: string;
examDate: string created_at: string;
reportDate: string updated_at: string;
technique: string
}
findings: string
conclusion: string
recommendations?: string
}
} }
export default function ReportsPage() { export default function ReportsPage() {
const [reports, setReports] = useState<Report[]>([]) const [reports, setReports] = useState<Report[]>([])
const [selectedReport, setSelectedReport] = useState<Report | null>(null) const [selectedReport, setSelectedReport] = useState<Report | null>(null)
const [isViewModalOpen, setIsViewModalOpen] = useState(false) const [isViewModalOpen, setIsViewModalOpen] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const requiredRole = useMemo(() => ["paciente"], []);
const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole });
useEffect(() => { useEffect(() => {
const mockReports: Report[] = [ if (user) {
{ const fetchReports = async () => {
id: "1", try {
title: "Exame de Sangue - Hemograma Completo", setIsLoading(true);
doctor: "Dr. João Silva", const fetchedReports = await reportsApi.getReports(user.id);
date: "2024-01-15", setReports(fetchedReports);
type: "Exame Laboratorial", } catch (error) {
status: "disponivel", console.error("Erro ao buscar laudos:", error)
description: "Hemograma completo com contagem de células sanguíneas", toast({
content: { title: "Erro ao buscar laudos",
patientInfo: { description: "Não foi possível carregar os laudos. Tente novamente.",
name: "Maria Silva Santos", variant: "destructive",
age: 35, })
gender: "Feminino", } finally {
id: "123.456.789-00", setIsLoading(false);
}, }
examDetails: { }
requestingDoctor: "Dr. João Silva - CRM 12345", fetchReports()
examDate: "15/01/2024", }
reportDate: "15/01/2024", }, [user?.id])
technique: "Análise automatizada com confirmação microscópica",
},
findings:
"Hemácias: 4.5 milhões/mm³ (VR: 4.0-5.2)\nHemoglobina: 13.2 g/dL (VR: 12.0-15.5)\nHematócrito: 40% (VR: 36-46)\nLeucócitos: 7.200/mm³ (VR: 4.000-11.000)\nPlaquetas: 280.000/mm³ (VR: 150.000-450.000)\n\nFórmula leucocitária:\n- Neutrófilos: 65% (VR: 50-70%)\n- Linfócitos: 28% (VR: 20-40%)\n- Monócitos: 5% (VR: 2-8%)\n- Eosinófilos: 2% (VR: 1-4%)",
conclusion:
"Hemograma dentro dos parâmetros normais. Não foram observadas alterações significativas na série vermelha, branca ou plaquetária.",
recommendations: "Manter acompanhamento médico regular. Repetir exame conforme orientação médica.",
},
},
{
id: "2",
title: "Radiografia do Tórax",
doctor: "Dra. Maria Santos",
date: "2024-01-10",
type: "Exame de Imagem",
status: "disponivel",
description: "Radiografia PA e perfil do tórax",
content: {
patientInfo: {
name: "Maria Silva Santos",
age: 35,
gender: "Feminino",
id: "123.456.789-00",
},
examDetails: {
requestingDoctor: "Dra. Maria Santos - CRM 67890",
examDate: "10/01/2024",
reportDate: "10/01/2024",
technique: "Radiografia digital PA e perfil",
},
findings:
"Campos pulmonares livres, sem sinais de consolidação ou derrame pleural. Silhueta cardíaca dentro dos limites normais. Estruturas ósseas íntegras. Diafragmas em posição normal.",
conclusion: "Radiografia de tórax sem alterações patológicas evidentes.",
recommendations: "Correlacionar com quadro clínico. Acompanhamento conforme indicação médica.",
},
},
{
id: "3",
title: "Eletrocardiograma",
doctor: "Dr. Carlos Oliveira",
date: "2024-01-08",
type: "Exame Cardiológico",
status: "pendente",
description: "ECG de repouso para avaliação cardíaca",
content: {
patientInfo: {
name: "Maria Silva Santos",
age: 35,
gender: "Feminino",
id: "123.456.789-00",
},
examDetails: {
requestingDoctor: "Dr. Carlos Oliveira - CRM 54321",
examDate: "08/01/2024",
reportDate: "",
technique: "ECG de repouso",
},
findings: "",
conclusion: "",
recommendations: "",
},
},
{
id: "4",
title: "Ultrassom Abdominal",
doctor: "Dra. Ana Costa",
date: "2024-01-05",
type: "Exame de Imagem",
status: "disponivel",
description: "Ultrassonografia do abdome total",
content: {
patientInfo: {
name: "Maria Silva Santos",
age: 35,
gender: "Feminino",
id: "123.456.789-00",
},
examDetails: {
requestingDoctor: "Dra. Ana Costa - CRM 98765",
examDate: "05/01/2024",
reportDate: "05/01/2024",
technique: "Ultrassom convencional",
},
findings:
"Viscerais bem posicionadas. Rim direito e esquerdo com contornos normais. Vesícula com volume dentro do normal.",
conclusion: "Ultrassom abdominal sem alterações patológicas evidentes.",
recommendations: "Acompanhamento conforme indicação médica.",
},
},
]
setReports(mockReports)
}, [])
const handleViewReport = (reportId: string) => { const handleViewReport = (reportId: string) => {
const report = reports.find((r) => r.id === reportId) const report = reports.find((r) => r.id === reportId)
@ -168,100 +76,23 @@ export default function ReportsPage() {
if (!report) return if (!report) return
try { try {
// Simular loading
toast({ toast({
title: "Preparando download...", title: "Preparando download...",
description: "Gerando PDF do laudo médico", description: "Gerando PDF do laudo médico",
}) })
// Criar conteúdo HTML do laudo para conversão em PDF const htmlContent = report.content_html;
const htmlContent = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Laudo Médico - ${report.title}</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 30px; }
.section { margin-bottom: 25px; }
.section-title { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }
.info-item { margin-bottom: 8px; }
.label { font-weight: bold; color: #555; }
.content { white-space: pre-line; }
.footer { margin-top: 40px; text-align: center; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="header">
<h1>LAUDO MÉDICO</h1>
<h2>${report.title}</h2>
<p><strong>Tipo:</strong> ${report.type}</p>
</div>
<div class="section">
<div class="section-title">DADOS DO PACIENTE</div>
<div class="info-grid">
<div class="info-item"><span class="label">Nome:</span> ${report.content.patientInfo.name}</div>
<div class="info-item"><span class="label">Idade:</span> ${report.content.patientInfo.age} anos</div>
<div class="info-item"><span class="label">Sexo:</span> ${report.content.patientInfo.gender}</div>
<div class="info-item"><span class="label">CPF:</span> ${report.content.patientInfo.id}</div>
</div>
</div>
<div class="section">
<div class="section-title">DETALHES DO EXAME</div>
<div class="info-grid">
<div class="info-item"><span class="label">Médico Solicitante:</span> ${report.content.examDetails.requestingDoctor}</div>
<div class="info-item"><span class="label">Data do Exame:</span> ${report.content.examDetails.examDate}</div>
<div class="info-item"><span class="label">Data do Laudo:</span> ${report.content.examDetails.reportDate}</div>
<div class="info-item"><span class="label">Técnica:</span> ${report.content.examDetails.technique}</div>
</div>
</div>
<div class="section">
<div class="section-title">ACHADOS</div>
<div class="content">${report.content.findings}</div>
</div>
<div class="section">
<div class="section-title">CONCLUSÃO</div>
<div class="content">${report.content.conclusion}</div>
</div>
${
report.content.recommendations
? `
<div class="section">
<div class="section-title">RECOMENDAÇÕES</div>
<div class="content">${report.content.recommendations}</div>
</div>
`
: ""
}
<div class="footer">
<p>Documento gerado em ${new Date().toLocaleDateString("pt-BR")} às ${new Date().toLocaleTimeString("pt-BR")}</p>
<p>Este é um documento médico oficial. Mantenha-o em local seguro.</p>
</div>
</body>
</html>
`
// Criar blob com o conteúdo HTML
const blob = new Blob([htmlContent], { type: "text/html" }) const blob = new Blob([htmlContent], { type: "text/html" })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
// Criar link temporário para download
const link = document.createElement("a") const link = document.createElement("a")
link.href = url link.href = url
link.download = `laudo-${report.title.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}-${report.date}.html` link.download = `laudo-${report.order_number}.html`
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
// Limpar URL temporária
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
toast({ toast({
@ -283,8 +114,18 @@ export default function ReportsPage() {
setSelectedReport(null) setSelectedReport(null)
} }
const availableReports = reports.filter((report) => report.status === "disponivel") const availableReports = reports.filter((report) => report.status.toLowerCase() === "draft")
const pendingReports = reports.filter((report) => report.status === "pendente") const pendingReports = reports.filter((report) => report.status.toLowerCase() !== "draft")
if (isLoading || isAuthLoading) {
return (
<Sidebar>
<div className="flex justify-center items-center h-full">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
</Sidebar>
)
}
return ( return (
<Sidebar> <Sidebar>
@ -333,25 +174,25 @@ export default function ReportsPage() {
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<CardTitle className="text-lg">{report.title}</CardTitle> <CardTitle className="text-lg">{report.exam}</CardTitle>
<CardDescription className="flex items-center gap-4"> <CardDescription className="flex items-center gap-4">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
{report.doctor} {report.requested_by}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
{new Date(report.date).toLocaleDateString("pt-BR")} {new Date(report.created_at).toLocaleDateString("pt-BR")}
</span> </span>
</CardDescription> </CardDescription>
</div> </div>
<Badge variant="secondary" className="bg-green-100 text-green-800"> <Badge variant="secondary" className="bg-green-100 text-green-800">
{report.type} Finalizado
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-gray-600 mb-4">{report.description}</p> <p className="text-gray-600 mb-4">{report.diagnosis}</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
@ -369,7 +210,7 @@ export default function ReportsPage() {
className="flex items-center gap-2" className="flex items-center gap-2"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
Baixar PDF Baixar
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@ -388,25 +229,25 @@ export default function ReportsPage() {
<CardHeader> <CardHeader>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="space-y-1"> <div className="space-y-1">
<CardTitle className="text-lg">{report.title}</CardTitle> <CardTitle className="text-lg">{report.exam}</CardTitle>
<CardDescription className="flex items-center gap-4"> <CardDescription className="flex items-center gap-4">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
{report.doctor} {report.requested_by}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
{new Date(report.date).toLocaleDateString("pt-BR")} {new Date(report.created_at).toLocaleDateString("pt-BR")}
</span> </span>
</CardDescription> </CardDescription>
</div> </div>
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800"> <Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
Pendente {report.status}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-gray-600 mb-4">{report.description}</p> <p className="text-gray-600 mb-4">{report.diagnosis}</p>
<p className="text-sm text-yellow-600 font-medium"> <p className="text-sm text-yellow-600 font-medium">
Laudo em processamento. Você será notificado quando estiver disponível. Laudo em processamento. Você será notificado quando estiver disponível.
</p> </p>
@ -417,7 +258,7 @@ export default function ReportsPage() {
</div> </div>
)} )}
{reports.length === 0 && ( {reports.length === 0 && !isLoading && (
<Card className="text-center py-12"> <Card className="text-center py-12">
<CardContent> <CardContent>
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
@ -432,9 +273,9 @@ export default function ReportsPage() {
<DialogHeader> <DialogHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<DialogTitle className="text-xl font-bold">{selectedReport?.title}</DialogTitle> <DialogTitle className="text-xl font-bold">{selectedReport?.exam}</DialogTitle>
<DialogDescription className="mt-1"> <DialogDescription className="mt-1">
{selectedReport?.type} - {selectedReport?.doctor} {selectedReport?.order_number}
</DialogDescription> </DialogDescription>
</div> </div>
<Button variant="ghost" size="sm" onClick={handleCloseModal} className="h-8 w-8 p-0"> <Button variant="ghost" size="sm" onClick={handleCloseModal} className="h-8 w-8 p-0">
@ -444,98 +285,11 @@ export default function ReportsPage() {
</DialogHeader> </DialogHeader>
{selectedReport && ( {selectedReport && (
<div className="space-y-6 mt-4"> <div className="space-y-6 mt-4" dangerouslySetInnerHTML={{ __html: selectedReport.content_html }} />
<Card>
<CardHeader>
<CardTitle className="text-lg">Dados do Paciente</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-500">Nome</p>
<p className="text-sm">{selectedReport.content.patientInfo.name}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">Idade</p>
<p className="text-sm">{selectedReport.content.patientInfo.age} anos</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">Sexo</p>
<p className="text-sm">{selectedReport.content.patientInfo.gender}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">CPF</p>
<p className="text-sm">{selectedReport.content.patientInfo.id}</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Detalhes do Exame</CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium text-gray-500">Médico Solicitante</p>
<p className="text-sm">{selectedReport.content.examDetails.requestingDoctor}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">Data do Exame</p>
<p className="text-sm">{selectedReport.content.examDetails.examDate}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">Data do Laudo</p>
<p className="text-sm">{selectedReport.content.examDetails.reportDate}</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500">Técnica</p>
<p className="text-sm">{selectedReport.content.examDetails.technique}</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Achados</CardTitle>
</CardHeader>
<CardContent>
<div className="whitespace-pre-line text-sm leading-relaxed">{selectedReport.content.findings}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Conclusão</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-relaxed">{selectedReport.content.conclusion}</p>
</CardContent>
</Card>
{selectedReport.content.recommendations && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Recomendações</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-relaxed">{selectedReport.content.recommendations}</p>
</CardContent>
</Card>
)}
<div className="flex gap-3 pt-4 border-t">
<Button onClick={() => handleDownloadReport(selectedReport.id)} className="flex items-center gap-2">
<Download className="h-4 w-4" />
Baixar PDF
</Button>
<Button variant="outline" onClick={handleCloseModal}>
Fechar
</Button>
</div>
</div>
)} )}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>
</Sidebar> </Sidebar>
) )
} }

View File

@ -8,7 +8,6 @@ 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, Loader2, MoreVertical } from "lucide-react"; import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react";
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";
import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import Sidebar from "@/components/Sidebar"; import Sidebar from "@/components/Sidebar";
@ -417,103 +416,83 @@ export default function PacientesPage() {
Ver detalhes Ver detalhes
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem asChild> <DropdownMenuItem asChild>
<Link <Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
href={`/secretary/pacientes/${patient.id}/editar`} <Edit className="w-4 h-4 mr-2" />
className="flex items-center w-full" Editar
> </Link>
<Edit className="w-4 h-4 mr-2" /> </DropdownMenuItem>
Editar
</Link>
</DropdownMenuItem>
<DropdownMenuItem> <DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" /> <Calendar className="w-4 h-4 mr-2" />
Marcar consulta Marcar consulta
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
className="text-red-600" <Trash2 className="w-4 h-4 mr-2" />
onClick={() => openDeleteDialog(String(patient.id))} Excluir
> </DropdownMenuItem>
<Trash2 className="w-4 h-4 mr-2" /> </DropdownMenuContent>
Excluir </DropdownMenu>
</DropdownMenuItem> </div>
</DropdownMenuContent> ))}
</DropdownMenu> </div>
)}
</div> </div>
))}
</div>
)}
</div>
{/* Paginação */} {/* Paginação */}
{totalPages > 1 && !loading && ( {totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200"> <div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<div className="flex space-x-2 flex-wrap justify-center"> <div className="flex space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
{" "} <Button
{/* Adicionado flex-wrap e justify-center para botões da paginação */} onClick={() => setPage((prev) => Math.max(1, prev - 1))}
<Button disabled={page === 1}
onClick={() => setPage((prev) => Math.max(1, prev - 1))} variant="outline"
disabled={page === 1} size="lg"
variant="outline" >
size="lg" &lt; Anterior
> </Button>
&lt; Anterior
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg"
className={
pageNumber === page
? "bg-blue-600 hover:bg-blue-700 text-white"
: "text-gray-700"
}
>
{pageNumber}
</Button>
))}
<Button
onClick={() =>
setPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={page === totalPages}
variant="outline"
size="lg"
>
Próximo &gt;
</Button>
</div>
</div>
)}
{/* AlertDialogs (Permanecem os mesmos) */} {Array.from({ length: totalPages }, (_, index) => index + 1)
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
<AlertDialogContent> .map((pageNumber) => (
<AlertDialogHeader> <Button
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle> key={pageNumber}
<AlertDialogDescription> onClick={() => setPage(pageNumber)}
Tem certeza que deseja excluir este paciente? Esta ação não pode variant={pageNumber === page ? "default" : "outline"}
ser desfeita. size="lg"
</AlertDialogDescription> className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
</AlertDialogHeader> >
<AlertDialogFooter> {pageNumber}
<AlertDialogCancel>Cancelar</AlertDialogCancel> </Button>
<AlertDialogAction ))}
onClick={() =>
patientToDelete && handleDeletePatient(patientToDelete) <Button
} onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
className="bg-red-600 hover:bg-red-700" disabled={page === totalPages}
> variant="outline"
Excluir size="lg"
</AlertDialogAction> >
</AlertDialogFooter> Próximo &gt;
</AlertDialogContent> </Button>
</AlertDialog> </div>
</div>
)}
{/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
<AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700">
Excluir
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog <AlertDialog
open={detailsDialogOpen} open={detailsDialogOpen}

View File

@ -9,7 +9,7 @@ export const reportsApi = {
return data; return data;
} catch (error) { } catch (error) {
console.error("Failed to fetch reports:", error); console.error("Failed to fetch reports:", error);
return []; throw error;
} }
}, },
getReportById: async (reportId) => { getReportById: async (reportId) => {