ordem alfabetica e visual

This commit is contained in:
m1guelmcf 2025-11-26 22:59:24 -03:00
parent 883411b8a3
commit dbc5a64ccd
2 changed files with 530 additions and 388 deletions

View File

@ -3,10 +3,39 @@
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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import {
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; DropdownMenu,
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react"; DropdownMenuContent,
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Plus,
Edit,
Trash2,
Eye,
Calendar,
Filter,
Loader2,
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";
@ -14,59 +43,60 @@ 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( const fetchAllPacientes = useCallback(async () => {
async () => { setLoading(true);
setLoading(true); setError(null);
setError(null); try {
try { // Como o backend retorna um array, chamamos sem paginação
// Como o backend retorna um array, chamamos sem paginação const res = await patientsService.list();
const res = await patientsService.list();
const mapped = res.map((p: any) => ({ const mapped = res.map((p: any) => ({
id: String(p.id ?? ""), id: String(p.id ?? ""),
nome: p.full_name ?? "—", nome: p.full_name ?? "—",
telefone: p.phone_mobile ?? p.phone1 ?? "—", telefone: p.phone_mobile ?? p.phone1 ?? "—",
cidade: p.city ?? "—", cidade: p.city ?? "—",
estado: p.state ?? "—", estado: p.state ?? "—",
// 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 ? p.next_appointment_at.split('T')[0].split('-').reverse().join('-') : "—", proximoAtendimento: p.next_appointment_at
vip: Boolean(p.vip ?? false), ? p.next_appointment_at.split("T")[0].split("-").reverse().join("-")
convenio: p.convenio ?? "Particular", // Define um valor padrão : "—",
status: p.status ?? undefined, vip: Boolean(p.vip ?? false),
})); convenio: p.convenio ?? "Particular", // Define um valor padrão
status: p.status ?? undefined,
}));
setAllPatients(mapped); setAllPatients(mapped);
} catch (e: any) { } catch (e: any) {
@ -77,32 +107,31 @@ 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" || convenioFilter === "all" || patient.convenio === convenioFilter;
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(() => {
@ -110,18 +139,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 {
@ -157,20 +186,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]">
@ -193,261 +222,331 @@ 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">VIP</span> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">
<Select value={vipFilter} onValueChange={setVipFilter}> VIP
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */} </span>
<SelectValue placeholder="VIP" /> <Select value={vipFilter} onValueChange={setVipFilter}>
</SelectTrigger> <SelectTrigger className="w-full sm:w-32">
<SelectContent> {" "}
<SelectItem value="all">Todos</SelectItem> {/* w-full para mobile, w-32 para sm+ */}
<SelectItem value="vip">VIP</SelectItem> <SelectValue placeholder="VIP" />
<SelectItem value="regular">Regular</SelectItem> </SelectTrigger>
</SelectContent> <SelectContent>
</Select> <SelectItem value="all">Todos</SelectItem>
</div> <SelectItem value="vip">VIP</SelectItem>
<SelectItem value="regular">Regular</SelectItem>
</SelectContent>
</Select>
</div>
{/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */} {/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */}
<Button variant="outline" className="w-full md:w-auto md:ml-auto"> <Button variant="outline" className="w-full md:w-auto md:ml-auto">
<Calendar className="w-4 h-4 mr-2" /> <Calendar className="w-4 h-4 mr-2" />
Aniversariantes Aniversariantes
</Button> </Button>
</div> </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+ */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block"> <div className="bg-white rounded-lg border border-gray-200 shadow-md hidden md:block">
<div className="overflow-x-auto"> {/* Permite rolagem horizontal se a tabela for muito larga */} <div className="overflow-x-auto">
{error ? ( {" "}
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div> {/* Permite rolagem horizontal se a tabela for muito larga */}
) : loading ? ( {error ? (
<div className="p-6 text-center text-gray-500 flex items-center justify-center"> <div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" /> Carregando pacientes... ) : loading ? (
<div className="p-6 text-center text-gray-500 flex items-center justify-center">
<Loader2 className="w-6 h-6 mr-2 animate-spin text-green-600" />{" "}
Carregando pacientes...
</div>
) : (
<table className="w-full min-w-[650px]">
{" "}
{/* min-w para evitar que a tabela se contraia demais */}
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">
Nome
</th>
{/* Ajustes de visibilidade de colunas para diferentes breakpoints */}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">
Telefone
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">
Cidade / Estado
</th>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">
Convênio
</th>
<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-blue-100 rounded-full flex items-center justify-center">
<span className="text-blue-600 font-medium text-sm">
{patient.nome?.charAt(0) || "?"}
</span>
</div> </div>
) : ( <span className="font-medium text-gray-900">
<table className="w-full min-w-[650px]"> {/* min-w para evitar que a tabela se contraia demais */} {patient.nome}
<thead className="bg-gray-50 border-b border-gray-200"> {patient.vip && (
<tr> <span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">
<th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th> VIP
{/* Ajustes de visibilidade de colunas para diferentes breakpoints */} </span>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Telefone</th> )}
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">Cidade / Estado</th> </span>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Convênio</th> </div>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Último atendimento</th> </td>
<th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Próximo atendimento</th> <td className="p-4 text-gray-600 hidden sm:table-cell">
<th className="text-left p-4 font-medium text-gray-700 w-[5%]">Ações</th> {patient.telefone}
</tr> </td>
</thead> <td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td>
<tbody> <td className="p-4 text-gray-600 hidden sm:table-cell">
{currentPatients.length === 0 ? ( {patient.convenio}
<tr> </td>
<td colSpan={7} className="p-8 text-center text-gray-500"> <td className="p-4 text-gray-600 hidden lg:table-cell">
{allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"} {patient.ultimoAtendimento}
</td> </td>
</tr> <td className="p-4 text-gray-600 hidden lg:table-cell">
) : ( {patient.proximoAtendimento}
currentPatients.map((patient) => ( </td>
<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>
<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"> <td className="p-4">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<div className="text-black-600 cursor-pointer"> <div className="text-black-600 cursor-pointer">
<MoreVertical className="h-4 w-4" /> <MoreVertical className="h-4 w-4" />
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}> <DropdownMenuItem
<Eye className="w-4 h-4 mr-2" /> onClick={() =>
Ver detalhes openDetailsDialog(String(patient.id))
</DropdownMenuItem> }
<DropdownMenuItem asChild> >
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full"> <Eye className="w-4 h-4 mr-2" />
<Edit className="w-4 h-4 mr-2" /> Ver detalhes
Editar </DropdownMenuItem>
</Link> <DropdownMenuItem asChild>
</DropdownMenuItem> <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> <DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" /> <Calendar className="w-4 h-4 mr-2" />
Marcar consulta Marcar consulta
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}> <DropdownMenuItem
<Trash2 className="w-4 h-4 mr-2" /> className="text-red-600"
Excluir onClick={() =>
</DropdownMenuItem> openDeleteDialog(String(patient.id))
</DropdownMenuContent> }
</DropdownMenu> >
</td> <Trash2 className="w-4 h-4 mr-2" />
</tr>
)))}
</tbody>
</table>
)}
</div>
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<div className="flex space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg"
>
&lt; Anterior
</Button>
{Array.from({ length: totalPages }, (_, index) => index + 1)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => (
<Button
key={pageNumber}
onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"}
size="lg"
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg"
>
Próximo &gt;
</Button>
</div>
</div>
)}
{/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<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 Excluir
</AlertDialogAction> </DropdownMenuItem>
</AlertDialogFooter> </DropdownMenuContent>
</AlertDialogContent> </DropdownMenu>
</AlertDialog> </td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
</div>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> {/* Paginação */}
<AlertDialogContent> {totalPages > 1 && !loading && (
<AlertDialogHeader> <div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle> <div className="flex space-x-2 flex-wrap justify-center">
<AlertDialogDescription> {" "}
{patientDetails === null ? ( {/* Adicionado flex-wrap e justify-center para botões da paginação */}
<div className="text-gray-500"> <Button
<Loader2 className="w-6 h-6 animate-spin mx-auto text-green-600 my-4" /> onClick={() => setPage((prev) => Math.max(1, prev - 1))}
Carregando... disabled={page === 1}
</div> variant="outline"
) : patientDetails?.error ? ( size="lg"
<div className="text-red-600 p-4">{patientDetails.error}</div> >
) : ( &lt; Anterior
<div className="grid gap-4 py-4"> </Button>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> {Array.from({ length: totalPages }, (_, index) => index + 1)
<div> .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
<p className="font-semibold">Nome Completo</p> .map((pageNumber) => (
<p>{patientDetails.full_name}</p> <Button
</div> key={pageNumber}
<div> onClick={() => setPage(pageNumber)}
<p className="font-semibold">Email</p> variant={pageNumber === page ? "default" : "outline"}
<p>{patientDetails.email}</p> size="lg"
</div> className={
<div> pageNumber === page
<p className="font-semibold">Telefone</p> ? "bg-blue-600 hover:bg-blue-700 text-white"
<p>{patientDetails.phone_mobile}</p> : "text-gray-700"
</div> }
<div> >
<p className="font-semibold">Data de Nascimento</p> {pageNumber}
<p>{patientDetails.birth_date}</p> </Button>
</div> ))}
<div> <Button
<p className="font-semibold">CPF</p> onClick={() =>
<p>{patientDetails.cpf}</p> setPage((prev) => Math.min(totalPages, prev + 1))
</div> }
<div> disabled={page === totalPages}
<p className="font-semibold">Tipo Sanguíneo</p> variant="outline"
<p>{patientDetails.blood_type}</p> size="lg"
</div> >
<div> Próximo &gt;
<p className="font-semibold">Peso (kg)</p> </Button>
<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-1 sm: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> </div>
</Sidebar> </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
open={detailsDialogOpen}
onOpenChange={setDetailsDialogOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
<AlertDialogDescription>
{patientDetails === null ? (
<div className="text-gray-500">
<Loader2 className="w-6 h-6 animate-spin mx-auto text-green-600 my-4" />
Carregando...
</div>
) : patientDetails?.error ? (
<div className="text-red-600 p-4">{patientDetails.error}</div>
) : (
<div className="grid gap-4 py-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<p className="font-semibold">Nome Completo</p>
<p>{patientDetails.full_name}</p>
</div>
<div>
<p className="font-semibold">Email</p>
<p>{patientDetails.email}</p>
</div>
<div>
<p className="font-semibold">Telefone</p>
<p>{patientDetails.phone_mobile}</p>
</div>
<div>
<p className="font-semibold">Data de Nascimento</p>
<p>{patientDetails.birth_date}</p>
</div>
<div>
<p className="font-semibold">CPF</p>
<p>{patientDetails.cpf}</p>
</div>
<div>
<p className="font-semibold">Tipo Sanguíneo</p>
<p>{patientDetails.blood_type}</p>
</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-1 sm: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>
</Sidebar>
);
} }

View File

@ -20,7 +20,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar";
import { format, addDays } from "date-fns"; import { format, addDays } from "date-fns";
import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react"; import { User, StickyNote, Check, ChevronsUpDown } from "lucide-react";
import { smsService } from "@/services/Sms.mjs";; import { smsService } from "@/services/Sms.mjs";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -277,7 +277,10 @@ export default function ScheduleForm() {
const patientId = isSecretaryLike ? selectedPatient : userId; const patientId = isSecretaryLike ? selectedPatient : userId;
if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) {
toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); toast({
title: "Campos obrigatórios",
description: "Preencha todos os campos.",
});
return; return;
} }
@ -302,7 +305,7 @@ export default function ScheduleForm() {
}.`, }.`,
}); });
let phoneNumber = "+5511999999999"; let phoneNumber = "+5511999999999";
try { try {
if (isSecretaryLike) { if (isSecretaryLike) {
@ -313,8 +316,12 @@ export default function ScheduleForm() {
const me = await usersService.getMe(); const me = await usersService.getMe();
const rawPhone = const rawPhone =
me?.profile?.phone || me?.profile?.phone ||
(typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || (typeof me?.profile === "object" && "phone_mobile" in me.profile
(typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || ? (me.profile as any).phone_mobile
: null) ||
(typeof me === "object" && "user_metadata" in me
? (me as any).user_metadata?.phone
: null) ||
null; null;
if (rawPhone) phoneNumber = rawPhone; if (rawPhone) phoneNumber = rawPhone;
} }
@ -399,14 +406,23 @@ export default function ScheduleForm() {
<CardTitle>Dados da Consulta</CardTitle> <CardTitle>Dados da Consulta</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <form
<div className="space-y-4"> {/* Ajuste: maior espaçamento vertical geral */} onSubmit={handleSubmit}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<div className="space-y-4">
{" "}
{/* Ajuste: maior espaçamento vertical geral */}
{/* Se secretária/gestor/admin → COMBOBOX de Paciente */} {/* Se secretária/gestor/admin → COMBOBOX de Paciente */}
{["secretaria", "gestor", "admin"].includes(role) && ( {["secretaria", "gestor", "admin"].includes(role) && (
<div className="flex flex-col gap-2"> {/* Ajuste: gap entre Label e Input */} <div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Paciente</Label> <Label>Paciente</Label>
<Popover open={openPatientCombobox} onOpenChange={setOpenPatientCombobox}> <Popover
open={openPatientCombobox}
onOpenChange={setOpenPatientCombobox}
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@ -415,7 +431,8 @@ export default function ScheduleForm() {
className="w-full justify-between" className="w-full justify-between"
> >
{selectedPatient {selectedPatient
? patients.find((p) => p.id === selectedPatient)?.full_name ? patients.find((p) => p.id === selectedPatient)
?.full_name
: "Selecione o paciente..."} : "Selecione o paciente..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@ -424,21 +441,29 @@ export default function ScheduleForm() {
<Command> <Command>
<CommandInput placeholder="Buscar paciente..." /> <CommandInput placeholder="Buscar paciente..." />
<CommandList> <CommandList>
<CommandEmpty>Nenhum paciente encontrado.</CommandEmpty> <CommandEmpty>
Nenhum paciente encontrado.
</CommandEmpty>
<CommandGroup> <CommandGroup>
{patients.map((patient) => ( {patients.map((patient) => (
<CommandItem <CommandItem
key={patient.id} key={patient.id}
value={patient.full_name} value={patient.full_name}
onSelect={() => { onSelect={() => {
setSelectedPatient(patient.id === selectedPatient ? "" : patient.id); setSelectedPatient(
patient.id === selectedPatient
? ""
: patient.id
);
setOpenPatientCombobox(false); setOpenPatientCombobox(false);
}} }}
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
selectedPatient === patient.id ? "opacity-100" : "opacity-0" selectedPatient === patient.id
? "opacity-100"
: "opacity-0"
)} )}
/> />
{patient.full_name} {patient.full_name}
@ -451,11 +476,15 @@ export default function ScheduleForm() {
</Popover> </Popover>
</div> </div>
)} )}
{/* COMBOBOX de Médico (Nova funcionalidade) */} {/* COMBOBOX de Médico (Nova funcionalidade) */}
<div className="flex flex-col gap-2"> {/* Ajuste: gap entre Label e Input */} <div className="flex flex-col gap-2">
{" "}
{/* Ajuste: gap entre Label e Input */}
<Label>Médico</Label> <Label>Médico</Label>
<Popover open={openDoctorCombobox} onOpenChange={setOpenDoctorCombobox}> <Popover
open={openDoctorCombobox}
onOpenChange={setOpenDoctorCombobox}
>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@ -467,7 +496,8 @@ export default function ScheduleForm() {
{loadingDoctors {loadingDoctors
? "Carregando médicos..." ? "Carregando médicos..."
: selectedDoctor : selectedDoctor
? doctors.find((d) => d.id === selectedDoctor)?.full_name ? doctors.find((d) => d.id === selectedDoctor)
?.full_name
: "Selecione o médico..."} : "Selecione o médico..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
@ -478,34 +508,47 @@ export default function ScheduleForm() {
<CommandList> <CommandList>
<CommandEmpty>Nenhum médico encontrado.</CommandEmpty> <CommandEmpty>Nenhum médico encontrado.</CommandEmpty>
<CommandGroup> <CommandGroup>
{doctors.map((doctor) => ( {[...doctors]
<CommandItem .sort((a, b) =>
key={doctor.id} String(a.full_name).localeCompare(
value={doctor.full_name} String(b.full_name)
onSelect={() => { )
setSelectedDoctor(doctor.id === selectedDoctor ? "" : doctor.id); )
setOpenDoctorCombobox(false); .map((doctor) => (
}} <CommandItem
> key={doctor.id}
<Check value={doctor.full_name}
className={cn( onSelect={() => {
"mr-2 h-4 w-4", setSelectedDoctor(
selectedDoctor === doctor.id ? "opacity-100" : "opacity-0" doctor.id === selectedDoctor
)} ? ""
/> : doctor.id
<div className="flex flex-col"> );
<span>{doctor.full_name}</span> setOpenDoctorCombobox(false);
<span className="text-xs text-muted-foreground">{doctor.specialty}</span> }}
</div> >
</CommandItem> <Check
))} className={cn(
"mr-2 h-4 w-4",
selectedDoctor === doctor.id
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{doctor.full_name}</span>
<span className="text-xs text-muted-foreground">
{doctor.specialty}
</span>
</div>
</CommandItem>
))}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Data</Label> <Label>Data</Label>
<div ref={calendarRef} className="rounded-lg border p-2"> <div ref={calendarRef} className="rounded-lg border p-2">
@ -528,7 +571,6 @@ export default function ScheduleForm() {
/> />
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Label>Observações</Label> <Label>Observações</Label>
<Textarea <Textarea
@ -540,7 +582,9 @@ export default function ScheduleForm() {
</div> </div>
</div> </div>
<div className="space-y-4"> {/* Ajuste: Espaçamento no lado direito também */} <div className="space-y-4">
{" "}
{/* Ajuste: Espaçamento no lado direito também */}
<Card className="shadow-md rounded-xl bg-blue-50 border border-blue-200"> <Card className="shadow-md rounded-xl bg-blue-50 border border-blue-200">
<CardHeader> <CardHeader>
<CardTitle className="text-blue-700">Resumo</CardTitle> <CardTitle className="text-blue-700">Resumo</CardTitle>
@ -596,7 +640,6 @@ export default function ScheduleForm() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
type="submit" type="submit"
@ -644,4 +687,4 @@ export default function ScheduleForm() {
)} )}
</div> </div>
); );
} }