Envolve a expressao filtered.map em React.Fragment no pacientes/page.tsx para tentar resolver um erro de parsing JSX no compilador do Next.js.
429 lines
15 KiB
TypeScript
429 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft, ShieldCheck } from "lucide-react";
|
|
|
|
import {
|
|
Paciente,
|
|
listarPacientes,
|
|
buscarPacientes,
|
|
buscarPacientePorId,
|
|
excluirPaciente,
|
|
listarAutorizacoesUsuario,
|
|
atualizarAutorizacoesUsuario,
|
|
type AuthorizationRole,
|
|
} from "@/lib/api";
|
|
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
|
import { getCurrentUser, atualizarPaciente } from "@/lib/api";
|
|
import {
|
|
UpdateAuthorizationsDialog,
|
|
type AuthorizationState,
|
|
} from "@/components/dialogs/update-authorizations-dialog";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
|
|
function normalizePaciente(p: any): Paciente {
|
|
return {
|
|
id: String(p.id ?? p.uuid ?? p.paciente_id ?? ""),
|
|
full_name: p.full_name ?? p.name ?? p.nome ?? "",
|
|
social_name: p.social_name ?? p.nome_social ?? null,
|
|
cpf: p.cpf ?? "",
|
|
rg: p.rg ?? p.document_number ?? null,
|
|
sex: p.sex ?? p.sexo ?? null,
|
|
birth_date: p.birth_date ?? p.data_nascimento ?? null,
|
|
phone_mobile: p.phone_mobile ?? p.telefone ?? "",
|
|
email: p.email ?? "",
|
|
cep: p.cep ?? "",
|
|
street: p.street ?? p.logradouro ?? "",
|
|
number: p.number ?? p.numero ?? "",
|
|
complement: p.complement ?? p.complemento ?? "",
|
|
neighborhood: p.neighborhood ?? p.bairro ?? "",
|
|
city: p.city ?? p.cidade ?? "",
|
|
state: p.state ?? p.estado ?? "",
|
|
notes: p.notes ?? p.observacoes ?? null,
|
|
user_id: p.user_id ?? p.usuario_id ?? p.userId ?? null,
|
|
};
|
|
}
|
|
|
|
|
|
export default function PacientesPage() {
|
|
const [patients, setPatients] = useState<Paciente[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [search, setSearch] = useState("");
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [editingId, setEditingId] = useState<string | null>(null);
|
|
const [viewingPatient, setViewingPatient] = useState<Paciente | null>(null);
|
|
const [authDialogOpen, setAuthDialogOpen] = useState(false);
|
|
const [authTargetPatient, setAuthTargetPatient] = useState<Paciente | null>(null);
|
|
const [authInitialRoles, setAuthInitialRoles] = useState<AuthorizationState | null>(null);
|
|
const [authorizationsLoading, setAuthorizationsLoading] = useState(false);
|
|
const [authorizationsError, setAuthorizationsError] = useState<string | null>(null);
|
|
const [authorizationsSubmitDisabled, setAuthorizationsSubmitDisabled] = useState(false);
|
|
|
|
const { toast } = useToast();
|
|
|
|
async function loadAll() {
|
|
try {
|
|
setLoading(true);
|
|
const data = await listarPacientes({ page: 1, limit: 20 });
|
|
|
|
if (Array.isArray(data)) {
|
|
setPatients(data.map(normalizePaciente));
|
|
} else {
|
|
setPatients([]);
|
|
}
|
|
setError(null);
|
|
} catch (e: any) {
|
|
setPatients([]);
|
|
setError(e?.message || "Erro ao carregar pacientes.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
loadAll();
|
|
}, []);
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!search.trim()) return patients;
|
|
const q = search.toLowerCase().trim();
|
|
const qDigits = q.replace(/\D/g, "");
|
|
|
|
return patients.filter((p) => {
|
|
// Busca por nome
|
|
const byName = (p.full_name || "").toLowerCase().includes(q);
|
|
|
|
// Busca por CPF (remove formatação)
|
|
const byCPF = qDigits.length >= 3 && (p.cpf || "").replace(/\D/g, "").includes(qDigits);
|
|
|
|
// Busca por ID (UUID completo ou parcial)
|
|
const byId = (p.id || "").toLowerCase().includes(q);
|
|
|
|
// Busca por email
|
|
const byEmail = (p.email || "").toLowerCase().includes(q);
|
|
|
|
return byName || byCPF || byId || byEmail;
|
|
});
|
|
}, [patients, search]);
|
|
|
|
function handleAdd() {
|
|
setEditingId(null);
|
|
setShowForm(true);
|
|
}
|
|
|
|
function handleEdit(id: string) {
|
|
setEditingId(id);
|
|
setShowForm(true);
|
|
}
|
|
|
|
function handleView(patient: Paciente) {
|
|
setViewingPatient(patient);
|
|
}
|
|
|
|
async function handleOpenAuthorizations(patient: Paciente) {
|
|
setAuthTargetPatient(patient);
|
|
setAuthDialogOpen(true);
|
|
setAuthorizationsLoading(!!patient.user_id);
|
|
setAuthorizationsError(null);
|
|
setAuthInitialRoles(null);
|
|
setAuthorizationsSubmitDisabled(false);
|
|
|
|
if (!patient.user_id) {
|
|
setAuthorizationsError(
|
|
"Este paciente ainda não possui um usuário vinculado. Cadastre ou vincule um usuário para gerenciar autorizações.",
|
|
);
|
|
setAuthInitialRoles({ paciente: true, medico: false });
|
|
setAuthorizationsSubmitDisabled(true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const roles = await listarAutorizacoesUsuario(patient.user_id);
|
|
if (!roles.length) {
|
|
setAuthInitialRoles({ paciente: true, medico: false });
|
|
} else {
|
|
setAuthInitialRoles({
|
|
paciente: roles.includes("paciente"),
|
|
medico: roles.includes("medico"),
|
|
});
|
|
}
|
|
} catch (e: any) {
|
|
setAuthorizationsError(e?.message || "Erro ao carregar autorizações.");
|
|
} finally {
|
|
setAuthorizationsLoading(false);
|
|
}
|
|
}
|
|
|
|
function handleAuthDialogOpenChange(open: boolean) {
|
|
if (!open) {
|
|
setAuthDialogOpen(false);
|
|
setAuthTargetPatient(null);
|
|
setAuthInitialRoles(null);
|
|
setAuthorizationsError(null);
|
|
setAuthorizationsLoading(false);
|
|
setAuthorizationsSubmitDisabled(false);
|
|
}
|
|
}
|
|
|
|
async function handleConfirmAuthorizations(selection: AuthorizationState) {
|
|
if (!authTargetPatient?.user_id) {
|
|
toast({
|
|
title: "Usuário não vinculado",
|
|
description: "Não foi possível atualizar as autorizações porque o usuário não está vinculado.",
|
|
variant: "destructive",
|
|
});
|
|
setAuthorizationsError(
|
|
"Vincule este paciente a um usuário antes de ajustar as autorizações.",
|
|
);
|
|
setAuthorizationsSubmitDisabled(true);
|
|
return;
|
|
}
|
|
|
|
console.log("[Auth] Confirm clicked", selection, "targetUserId=", authTargetPatient?.user_id);
|
|
setAuthorizationsLoading(true);
|
|
setAuthorizationsError(null);
|
|
|
|
const selectedRoles: AuthorizationRole[] = [];
|
|
if (selection.paciente) selectedRoles.push("paciente");
|
|
if (selection.medico) selectedRoles.push("medico");
|
|
|
|
try {
|
|
console.log("[Auth] Updating roles to server:", selectedRoles);
|
|
const result = await atualizarAutorizacoesUsuario(authTargetPatient.user_id, selectedRoles);
|
|
console.log("[Auth] Update result:", result);
|
|
toast({
|
|
title: "Autorizações atualizadas",
|
|
description: "As permissões deste paciente foram atualizadas com sucesso.",
|
|
});
|
|
setAuthDialogOpen(false);
|
|
setAuthTargetPatient(null);
|
|
setAuthInitialRoles(null);
|
|
await loadAll();
|
|
} catch (e: any) {
|
|
toast({
|
|
title: "Erro ao atualizar autorizações",
|
|
description: e?.message || "Não foi possível atualizar as autorizações.",
|
|
variant: "destructive",
|
|
});
|
|
} finally {
|
|
setAuthorizationsLoading(false);
|
|
}
|
|
}
|
|
|
|
async function handleDelete(id: string) {
|
|
if (!confirm("Excluir este paciente?")) return;
|
|
try {
|
|
await excluirPaciente(id);
|
|
setPatients((prev) => prev.filter((x) => String(x.id) !== String(id)));
|
|
} catch (e: any) {
|
|
alert(e?.message || "Não foi possível excluir.");
|
|
}
|
|
}
|
|
|
|
async function handleSaved(p: Paciente) {
|
|
// Normaliza e atualiza localmente
|
|
let saved = normalizePaciente(p);
|
|
// Vincula o registro de paciente ao usuário autenticado
|
|
try {
|
|
const user = await getCurrentUser();
|
|
// Preparar payload apenas com campos de PacienteInput e user_id
|
|
const { id: _id, user_id: _oldUserId, ...rest } = saved;
|
|
const payload = { ...rest, user_id: user.id };
|
|
const linked = await atualizarPaciente(saved.id, payload);
|
|
saved = normalizePaciente(linked);
|
|
} catch (e) {
|
|
// Se falhar, mantém saved original
|
|
console.warn("Falha ao vincular usuário ao paciente:", e);
|
|
}
|
|
// Atualiza lista com o registro vinculado
|
|
setPatients((prev) => {
|
|
const i = prev.findIndex((x) => String(x.id) === String(saved.id));
|
|
if (i < 0) return [saved, ...prev];
|
|
const clone = [...prev];
|
|
clone[i] = saved;
|
|
return clone;
|
|
});
|
|
setShowForm(false);
|
|
}
|
|
|
|
async function handleBuscarServidor() {
|
|
const q = search.trim();
|
|
if (!q) return loadAll();
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Se parece com ID (UUID), busca diretamente
|
|
if (q.includes('-') && q.length > 10) {
|
|
const one = await buscarPacientePorId(q);
|
|
setPatients(one ? [normalizePaciente(one)] : []);
|
|
setError(one ? null : "Paciente não encontrado.");
|
|
// Limpa o campo de busca para que o filtro não interfira
|
|
setSearch("");
|
|
return;
|
|
}
|
|
|
|
// Para outros termos, usa busca avançada
|
|
const results = await buscarPacientes(q);
|
|
setPatients(results.map(normalizePaciente));
|
|
setError(results.length === 0 ? "Nenhum paciente encontrado." : null);
|
|
// Limpa o campo de busca para que o filtro não interfira
|
|
setSearch("");
|
|
|
|
} catch (e: any) {
|
|
setPatients([]);
|
|
setError(e?.message || "Erro na busca.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
function handleBuscarClick() {
|
|
void handleBuscarServidor();
|
|
}
|
|
|
|
if (loading) return <p>Carregando pacientes...</p>;
|
|
if (error) return <p className="text-red-500">{error}</p>;
|
|
|
|
if (showForm) {
|
|
return (
|
|
<div className="space-y-6 p-6 bg-background">
|
|
<div className="flex items-center gap-4">
|
|
<Button variant="ghost" onClick={() => setShowForm(false)}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<h1 className="text-2xl font-bold">{editingId ? "Editar paciente" : "Novo paciente"}</h1>
|
|
</div>
|
|
|
|
<PatientRegistrationForm
|
|
inline
|
|
mode={editingId ? "edit" : "create"}
|
|
patientId={editingId}
|
|
onSaved={handleSaved}
|
|
onClose={() => setShowForm(false)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
'[Page] Rendering dialog, passing onConfirm. Typeof handleConfirmAuthorizations is:',
|
|
typeof handleConfirmAuthorizations,
|
|
);
|
|
|
|
return (
|
|
<div className="flex h-full flex-col space-y-6 p-6 bg-background">
|
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">Pacientes</h1>
|
|
<p className="text-muted-foreground">Gerencie os pacientes</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
className="pl-8 w-80"
|
|
placeholder="Buscar por nome, CPF ou ID…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
|
|
/>
|
|
</div>
|
|
<Button variant="secondary" onClick={handleBuscarClick} className="hover:bg-primary hover:text-white">Buscar</Button>
|
|
<Button onClick={handleAdd}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Novo paciente
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nome</TableHead>
|
|
<TableHead>CPF</TableHead>
|
|
<TableHead>Telefone</TableHead>
|
|
<TableHead>Cidade</TableHead>
|
|
<TableHead>Estado</TableHead>
|
|
<TableHead className="w-[100px]">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filtered.length > 0 ? (
|
|
filtered.map((p) => (
|
|
<TableRow key={p.id}>
|
|
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell>
|
|
<TableCell>{p.cpf || "-"}</TableCell>
|
|
<TableCell>{p.phone_mobile || "-"}</TableCell>
|
|
<TableCell>{p.city || "-"}</TableCell>
|
|
<TableCell>{p.state || "-"}</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-accent">
|
|
<span className="sr-only">Abrir menu</span>
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleView(p)}>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
Ver
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleEdit(String(p.id))}>
|
|
<Edit className="mr-2 h-4 w-4" />
|
|
Editar
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleOpenAuthorizations(p)}>
|
|
<ShieldCheck className="mr-2 h-4 w-4" />
|
|
Atualizar autorizações
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
Excluir
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
|
Nenhum paciente encontrado
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div className="text-sm text-muted-foreground">Mostrando {filtered.length} de {patients.length}</div>
|
|
|
|
<UpdateAuthorizationsDialog
|
|
open={authDialogOpen}
|
|
entityType="paciente"
|
|
entityName={authTargetPatient?.full_name}
|
|
initialRoles={authInitialRoles ?? undefined}
|
|
loading={authorizationsLoading}
|
|
error={authorizationsError}
|
|
disableSubmit={authorizationsSubmitDisabled}
|
|
onOpenChange={handleAuthDialogOpenChange}
|
|
onConfirm={handleConfirmAuthorizations}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|