Ultimos Ajustes

This commit is contained in:
GagoDuBroca 2025-11-06 10:59:57 -03:00
parent 1daa664ff4
commit 3549cab396
6 changed files with 437 additions and 1105 deletions

View File

@ -197,7 +197,7 @@ export default function PacientesPage() {
</SelectContent> </SelectContent>
</Select> </Select>
<Link href="/doctor/pacientes/novo"> <Link href="/doctor/pacientes/novo">
<Button variant="default" className="bg-primary hover:bg-primary/90"> <Button variant="default" className="bg-green-600 hover:bg-green-700">
Novo Paciente Novo Paciente
</Button> </Button>
</Link> </Link>
@ -334,7 +334,7 @@ export default function PacientesPage() {
onClick={() => paginate(number)} onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-border ${ className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-border ${
currentPage === number currentPage === number
? "bg-primary text-primary-foreground shadow-md border-primary" ? "bg-green-600 text-primary-foreground shadow-md border-green-600"
: "bg-secondary text-secondary-foreground hover:bg-secondary/80" : "bg-secondary text-secondary-foreground hover:bg-secondary/80"
}`} }`}
> >

View File

@ -202,12 +202,6 @@ export default function DoctorsPage() {
<h1 className="text-2xl font-bold text-gray-900">Médicos Cadastrados</h1> <h1 className="text-2xl font-bold text-gray-900">Médicos Cadastrados</h1>
<p className="text-sm text-gray-500">Gerencie todos os profissionais de saúde.</p> <p className="text-sm text-gray-500">Gerencie todos os profissionais de saúde.</p>
</div> </div>
<Link href="/manager/home/novo" className="w-full sm:w-auto">
<Button className="w-full sm:w-auto bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
Adicionar Novo
</Button>
</Link>
</div> </div>

View File

@ -1,676 +0,0 @@
"use client";
import type React from "react";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Upload, Plus, X, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { useToast } from "@/hooks/use-toast";
import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
export default function NovoPacientePage() {
const [anexosOpen, setAnexosOpen] = useState(false);
const [anexos, setAnexos] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const { toast } = useToast();
const adicionarAnexo = () => {
setAnexos([...anexos, `Documento ${anexos.length + 1}`]);
};
const removerAnexo = (index: number) => {
setAnexos(anexos.filter((_, i) => i !== index));
};
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
const formatCPF = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
return cleaned.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
};
const formatCEP = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 8);
return cleaned.replace(/(\d{5})(\d{3})/, '$1-$2');
};
const formatPhoneMobile = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length > 10) {
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '+55 ($1) $2-$3');
}
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '+55 ($1) $2-$3');
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isLoading) return;
setIsLoading(true);
const form = e.currentTarget;
const formData = new FormData(form);
const apiPayload = {
full_name: (formData.get("nome") as string) || "", // obrigatório
social_name: (formData.get("nomeSocial") as string) || undefined,
cpf: (formatCPF(formData.get("cpf") as string)) || "", // obrigatório
email: (formData.get("email") as string) || "", // obrigatório
phone_mobile: (formatPhoneMobile(formData.get("celular") as string)) || "", // obrigatório
birth_date: formData.get("dataNascimento") ? new Date(formData.get("dataNascimento") as string) : undefined,
sex: (formData.get("sexo") as string) || undefined,
blood_type: (formData.get("tipoSanguineo") as string) || undefined,
weight_kg: formData.get("peso") ? parseFloat(formData.get("peso") as string) : undefined,
height_m: formData.get("altura") ? parseFloat(formData.get("altura") as string) : undefined,
cep: (formatCEP(formData.get("cep") as string)) || undefined,
street: (formData.get("endereco") as string) || undefined,
number: (formData.get("numero") as string) || undefined,
complement: (formData.get("complemento") as string) || undefined,
neighborhood: (formData.get("bairro") as string) || undefined,
city: (formData.get("cidade") as string) || undefined,
state: (formData.get("estado") as string) || undefined,
};
console.log(apiPayload.email)
console.log(apiPayload.cep)
console.log(apiPayload.phone_mobile)
const errors: string[] = [];
const fullName = apiPayload.full_name?.trim() || "";
if (!fullName || fullName.length < 2 || fullName.length > 255) {
errors.push("Nome deve ter entre 2 e 255 caracteres.");
}
const cpf = apiPayload.cpf || "";
if (!/^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(cpf)) {
errors.push("CPF deve estar no formato XXX.XXX.XXX-XX.");
}
const sex = apiPayload.sex;
const allowedSex = ["Masculino", "Feminino", "outro"];
if (!sex || !allowedSex.includes(sex)) {
errors.push("Sexo é obrigatório e deve ser masculino, feminino ou outro.");
}
if (!apiPayload.birth_date) {
errors.push("Data de nascimento é obrigatória.");
}
const phoneMobile = apiPayload.phone_mobile || "";
if (phoneMobile && !/^\+55 \(\d{2}\) \d{4,5}-\d{4}$/.test(phoneMobile)) {
errors.push("Celular deve estar no formato +55 (XX) XXXXX-XXXX.");
}
const cep = apiPayload.cep || "";
if (cep && !/^\d{5}-\d{3}$/.test(cep)) {
errors.push("CEP deve estar no formato XXXXX-XXX.");
}
const state = apiPayload.state || "";
if (state && state.length !== 2) {
errors.push("Estado (UF) deve ter 2 caracteres.");
}
if (errors.length) {
toast({ title: "Corrija os campos", description: errors[0] });
console.log("campos errados")
setIsLoading(false);
return;
}
try {
const res = await patientsService.create(apiPayload);
console.log(res)
let message = "Paciente cadastrado com sucesso";
try {
if (!res[0].id) {
throw new Error(`${res.error} ${res.message}`|| "A API retornou erro");
} else {
console.log(message)
}
} catch {}
toast({
title: "Sucesso",
description: message,
});
router.push("/manager/pacientes");
} catch (err: any) {
toast({
title: "Erro",
description: err?.message || "Não foi possível cadastrar o paciente",
});
} finally {
setIsLoading(false);
}
};
return (
<SecretaryLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Novo Paciente</h1>
<p className="text-gray-600">Cadastre um novo paciente no sistema</p>
</div>
</div>
<form className="space-y-6" onSubmit={handleSubmit}>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Dados Pessoais</h2>
<div className="space-y-6">
<div className="flex items-center gap-4">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center">
<Upload className="w-8 h-8 text-gray-400" />
</div>
<Button variant="outline" type="button" size="sm">
<Upload className="w-4 h-4 mr-2" />
Carregar Foto
</Button>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="nome" className="text-sm font-medium text-gray-700">
Nome *
</Label>
<Input id="nome" name="nome" placeholder="Nome completo" required className="mt-1" />
</div>
<div>
<Label htmlFor="nomeSocial" className="text-sm font-medium text-gray-700">
Nome Social
</Label>
<Input id="nomeSocial" name="nomeSocial" placeholder="Nome social ou apelido" className="mt-1" />
</div>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label htmlFor="cpf" className="text-sm font-medium text-gray-700">
CPF *
</Label>
<Input id="cpf" name="cpf" placeholder="000.000.000-00" required className="mt-1" />
</div>
<div>
<Label htmlFor="rg" className="text-sm font-medium text-gray-700">
RG
</Label>
<Input id="rg" name="rg" placeholder="00.000.000-0" className="mt-1" />
</div>
<div>
<Label htmlFor="outrosDocumentos" className="text-sm font-medium text-gray-700">
Outros Documentos
</Label>
<Select name="outrosDocumentos">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cnh">CNH</SelectItem>
<SelectItem value="passaporte">Passaporte</SelectItem>
<SelectItem value="carteira-trabalho">Carteira de Trabalho</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label className="text-sm font-medium text-gray-700">Sexo *</Label>
<div className="flex gap-4 mt-2">
<label className="flex items-center gap-2">
<input type="radio" name="sexo" value="Masculino" className="text-blue-600" required/>
<span className="text-sm">Masculino</span>
</label>
<label className="flex items-center gap-2">
<input type="radio" name="sexo" value="Feminino" className="text-blue-600" required/>
<span className="text-sm">Feminino</span>
</label>
</div>
</div>
<div>
<Label htmlFor="dataNascimento" className="text-sm font-medium text-gray-700">
Data de Nascimento *
</Label>
<Input id="dataNascimento" name="dataNascimento" type="date" className="mt-1" required/>
</div>
<div>
<Label htmlFor="estadoCivil" className="text-sm font-medium text-gray-700">
Estado Civil
</Label>
<Select name="estadoCivil">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="solteiro">Solteiro(a)</SelectItem>
<SelectItem value="casado">Casado(a)</SelectItem>
<SelectItem value="divorciado">Divorciado(a)</SelectItem>
<SelectItem value="viuvo">Viúvo(a)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="etnia" className="text-sm font-medium text-gray-700">
Etnia
</Label>
<Select name="etnia">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="branca">Branca</SelectItem>
<SelectItem value="preta">Preta</SelectItem>
<SelectItem value="parda">Parda</SelectItem>
<SelectItem value="amarela">Amarela</SelectItem>
<SelectItem value="indigena">Indígena</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="raca" className="text-sm font-medium text-gray-700">
Raça
</Label>
<Select name="raca">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="branca">Branca</SelectItem>
<SelectItem value="preta">Preta</SelectItem>
<SelectItem value="parda">Parda</SelectItem>
<SelectItem value="amarela">Amarela</SelectItem>
<SelectItem value="indigena">Indígena</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="naturalidade" className="text-sm font-medium text-gray-700">
Naturalidade
</Label>
<Select name="naturalidade">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="aracaju">Aracaju</SelectItem>
<SelectItem value="salvador">Salvador</SelectItem>
<SelectItem value="recife">Recife</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="nacionalidade" className="text-sm font-medium text-gray-700">
Nacionalidade
</Label>
<Select name="nacionalidade">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="brasileira">Brasileira</SelectItem>
<SelectItem value="estrangeira">Estrangeira</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="profissao" className="text-sm font-medium text-gray-700">
Profissão
</Label>
<Input id="profissao" name="profissao" placeholder="Profissão" className="mt-1" />
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="nomeMae" className="text-sm font-medium text-gray-700">
Nome da Mãe
</Label>
<Input id="nomeMae" name="nomeMae" placeholder="Nome da mãe" className="mt-1" />
</div>
<div>
<Label htmlFor="profissaoMae" className="text-sm font-medium text-gray-700">
Profissão da Mãe
</Label>
<Input id="profissaoMae" name="profissaoMae" placeholder="Profissão da mãe" className="mt-1" />
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="nomePai" className="text-sm font-medium text-gray-700">
Nome do Pai
</Label>
<Input id="nomePai" name="nomePai" placeholder="Nome do pai" className="mt-1" />
</div>
<div>
<Label htmlFor="profissaoPai" className="text-sm font-medium text-gray-700">
Profissão do Pai
</Label>
<Input id="profissaoPai" name="profissaoPai" placeholder="Profissão do pai" className="mt-1" />
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="nomeResponsavel" className="text-sm font-medium text-gray-700">
Nome do Responsável
</Label>
<Input id="nomeResponsavel" name="nomeResponsavel" placeholder="Nome do responsável" className="mt-1" />
</div>
<div>
<Label htmlFor="cpfResponsavel" className="text-sm font-medium text-gray-700">
CPF do Responsável
</Label>
<Input id="cpfResponsavel" name="cpfResponsavel" placeholder="000.000.000-00" className="mt-1" />
</div>
</div>
<div>
<Label htmlFor="nomeEsposo" className="text-sm font-medium text-gray-700">
Nome do Esposo(a)
</Label>
<Input id="nomeEsposo" name="nomeEsposo" placeholder="Nome do esposo(a)" className="mt-1" />
</div>
<div className="flex items-center space-x-2">
<Checkbox id="rnGuia" name="rnGuia" />
<Label htmlFor="rnGuia" className="text-sm text-gray-700">
RN na Guia do convênio
</Label>
</div>
<div>
<Label htmlFor="codigoLegado" className="text-sm font-medium text-gray-700">
Código Legado
</Label>
<Input id="codigoLegado" name="codigoLegado" placeholder="Código do sistema anterior" className="mt-1" />
</div>
<div>
<Label htmlFor="observacoes" className="text-sm font-medium text-gray-700">
Observações
</Label>
<Textarea id="observacoes" name="observacoes" placeholder="Observações gerais sobre o paciente" className="min-h-[100px] mt-1" />
</div>
<Collapsible open={anexosOpen} onOpenChange={setAnexosOpen}>
<CollapsibleTrigger asChild>
<Button variant="ghost" type="button" className="w-full justify-between p-0 h-auto text-left">
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-400 rounded-sm flex items-center justify-center">
<span className="text-white text-xs">📎</span>
</div>
<span className="text-sm font-medium text-gray-700">Anexos do paciente</span>
</div>
<ChevronDown className={`w-4 h-4 transition-transform ${anexosOpen ? "rotate-180" : ""}`} />
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-4">
{anexos.map((anexo, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg bg-gray-50">
<span className="text-sm">{anexo}</span>
<Button variant="ghost" size="sm" onClick={() => removerAnexo(index)} type="button">
<X className="w-4 h-4" />
</Button>
</div>
))}
<Button variant="outline" onClick={adicionarAnexo} type="button" size="sm">
<Plus className="w-4 h-4 mr-2" />
Adicionar Anexo
</Button>
</CollapsibleContent>
</Collapsible>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Contato</h2>
<div className="space-y-4">
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label htmlFor="email" className="text-sm font-medium text-gray-700">
E-mail *
</Label>
<Input id="email" name="email" type="email" placeholder="email@exemplo.com" className="mt-1" required/>
</div>
<div>
<Label htmlFor="celular" className="text-sm font-medium text-gray-700">
Celular *
</Label>
<div className="flex mt-1">
<Select>
<SelectTrigger className="w-20 rounded-r-none">
<SelectValue placeholder="+55" />
</SelectTrigger>
<SelectContent>
<SelectItem value="+55">+55</SelectItem>
</SelectContent>
</Select>
<Input id="celular" name="celular" placeholder="(XX) XXXXX-XXXX" className="rounded-l-none" required/>
</div>
</div>
<div>
<Label htmlFor="telefone1" className="text-sm font-medium text-gray-700">
Telefone 1
</Label>
<Input id="telefone1" name="telefone1" placeholder="(XX) XXXX-XXXX" className="mt-1" />
</div>
</div>
<div>
<Label htmlFor="telefone2" className="text-sm font-medium text-gray-700">
Telefone 2
</Label>
<Input id="telefone2" name="telefone2" placeholder="(XX) XXXX-XXXX" className="mt-1" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Endereço</h2>
<div className="space-y-4">
<div>
<Label htmlFor="cep" className="text-sm font-medium text-gray-700">
CEP
</Label>
<Input id="cep" name="cep" placeholder="00000-000" className="mt-1 max-w-xs" />
</div>
<div className="grid md:grid-cols-3 gap-4">
<div className="md:col-span-2">
<Label htmlFor="endereco" className="text-sm font-medium text-gray-700">
Endereço
</Label>
<Input id="endereco" name="endereco" placeholder="Rua, Avenida..." className="mt-1" />
</div>
<div>
<Label htmlFor="numero" className="text-sm font-medium text-gray-700">
Número
</Label>
<Input id="numero" name="numero" placeholder="123" className="mt-1" />
</div>
</div>
<div>
<Label htmlFor="complemento" className="text-sm font-medium text-gray-700">
Complemento
</Label>
<Input id="complemento" name="complemento" placeholder="Apto, Bloco..." className="mt-1" />
</div>
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label htmlFor="bairro" className="text-sm font-medium text-gray-700">
Bairro
</Label>
<Input id="bairro" name="bairro" placeholder="Bairro" className="mt-1" />
</div>
<div>
<Label htmlFor="cidade" className="text-sm font-medium text-gray-700">
Cidade
</Label>
<Input id="cidade" name="cidade" placeholder="Cidade" className="mt-1" />
</div>
<div>
<Label htmlFor="estado" className="text-sm font-medium text-gray-700">
Estado
</Label>
<Select name="estado">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="SE">Sergipe</SelectItem>
<SelectItem value="BA">Bahia</SelectItem>
<SelectItem value="AL">Alagoas</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Informações Médicas</h2>
<div className="space-y-4">
<div className="grid md:grid-cols-4 gap-4">
<div>
<Label htmlFor="tipoSanguineo" className="text-sm font-medium text-gray-700">
Tipo Sanguíneo
</Label>
<Select name="tipoSanguineo">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="A+">A+</SelectItem>
<SelectItem value="A-">A-</SelectItem>
<SelectItem value="B+">B+</SelectItem>
<SelectItem value="B-">B-</SelectItem>
<SelectItem value="AB+">AB+</SelectItem>
<SelectItem value="AB-">AB-</SelectItem>
<SelectItem value="O+">O+</SelectItem>
<SelectItem value="O-">O-</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="peso" className="text-sm font-medium text-gray-700">
Peso
</Label>
<div className="relative mt-1">
<Input id="peso" name="peso" type="number" placeholder="70" />
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-sm text-gray-500">kg</span>
</div>
</div>
<div>
<Label htmlFor="altura" className="text-sm font-medium text-gray-700">
Altura
</Label>
<div className="relative mt-1">
<Input id="altura" name="altura" type="number" step="0.01" placeholder="1.70" />
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-sm text-gray-500">m</span>
</div>
</div>
<div>
<Label htmlFor="imc" className="text-sm font-medium text-gray-700">
IMC
</Label>
<div className="relative mt-1">
<Input id="imc" name="imc" placeholder="Calculado automaticamente" disabled />
<span className="absolute right-3 top-1/2 transform -translate-y-1/2 text-sm text-gray-500">kg/m²</span>
</div>
</div>
</div>
<div>
<Label htmlFor="alergias" className="text-sm font-medium text-gray-700">
Alergias
</Label>
<Textarea id="alergias" name="alergias" placeholder="Ex: AAS, Dipirona, etc." className="min-h-[80px] mt-1" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Informações de Convênio</h2>
<div className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="convenio" className="text-sm font-medium text-gray-700">
Convênio
</Label>
<Select name="convenio">
<SelectTrigger className="mt-1">
<SelectValue placeholder="Selecione" />
</SelectTrigger>
<SelectContent>
<SelectItem value="particular">Particular</SelectItem>
<SelectItem value="sus">SUS</SelectItem>
<SelectItem value="unimed">Unimed</SelectItem>
<SelectItem value="bradesco">Bradesco Saúde</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="plano" className="text-sm font-medium text-gray-700">
Plano
</Label>
<Input id="plano" name="plano" placeholder="Nome do plano" className="mt-1" />
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div>
<Label htmlFor="numeroMatricula" className="text-sm font-medium text-gray-700">
de Matrícula
</Label>
<Input id="numeroMatricula" name="numeroMatricula" placeholder="Número da matrícula" className="mt-1" />
</div>
<div>
<Label htmlFor="validadeCarteira" className="text-sm font-medium text-gray-700">
Validade da Carteira
</Label>
<Input id="validadeCarteira" name="validadeCarteira" type="date" className="mt-1" />
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="validadeIndeterminada" name="validadeIndeterminada" />
<Label htmlFor="validadeIndeterminada" className="text-sm text-gray-700">
Validade Indeterminada
</Label>
</div>
</div>
</div>
<div className="flex justify-end gap-4">
<Link href="/manager/pacientes">
<Button variant="outline" type="button">
Cancelar
</Button>
</Link>
<Button type="submit" className="bg-blue-600 hover:bg-blue-700" disabled={isLoading}>
{isLoading ? "Salvando..." : "Salvar Paciente"}
</Button>
</div>
</form>
</div>
</SecretaryLayout>
);
}

View File

@ -1,430 +1,454 @@
"use client"; "use client";
import React, { useState, useEffect, useCallback, useMemo } 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,
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 } from "lucide-react"; import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2 } from "lucide-react";
import { import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import ManagerLayout from "@/components/manager-layout";
import { patientsService } from "@/services/patientsApi.mjs"; import { patientsService } from "@/services/patientsApi.mjs";
import ManagerLayout from "@/components/manager-layout";
const formatDate = (dateString: string | null | undefined): string => { // Defina o tamanho da página.
if (!dateString) { const PAGE_SIZE = 5;
return "N/A";
}
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
return "Data inválida";
}
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
} catch (error) {
return "Data inválida";
}
};
export default function PacientesPage() { export default function PacientesPage() {
const [searchTerm, setSearchTerm] = useState(""); // --- ESTADOS DE DADOS E GERAL ---
const [convenioFilter, setConvenioFilter] = useState("all"); const [searchTerm, setSearchTerm] = useState("");
const [vipFilter, setVipFilter] = useState("all"); const [convenioFilter, setConvenioFilter] = useState("all");
const [patients, setPatients] = useState<any[]>([]); const [vipFilter, setVipFilter] = useState("all");
const [loading, setLoading] = useState(true); // Alterado para true
const [error, setError] = useState<string | null>(null);
// --- Estados de Paginação (ADICIONADOS) --- // Lista completa, carregada da API uma única vez
const [itemsPerPage, setItemsPerPage] = useState(10); const [allPatients, setAllPatients] = useState<any[]>([]);
const [currentPage, setCurrentPage] = useState(1); // Lista após a aplicação dos filtros (base para a paginação)
// --------------------------------------------- const [filteredPatients, setFilteredPatients] = useState<any[]>([]);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [loading, setLoading] = useState(true);
const [patientToDelete, setPatientToDelete] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null);
const openDetailsDialog = async (patientId: string) => { // --- ESTADOS DE PAGINAÇÃO ---
setDetailsDialogOpen(true); const [page, setPage] = useState(1);
setPatientDetails(null);
try {
const res = await patientsService.getById(patientId);
setPatientDetails(res[0]);
} catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
}
};
// --- LÓGICA DE BUSCA DE DADOS (ATUALIZADA) --- // CÁLCULO DA PAGINAÇÃO
const fetchPacientes = useCallback(async () => { const totalPages = Math.ceil(filteredPatients.length / PAGE_SIZE);
setLoading(true); const startIndex = (page - 1) * PAGE_SIZE;
setError(null); const endIndex = startIndex + PAGE_SIZE;
try { // Pacientes a serem exibidos na tabela (aplicando a paginação)
const res = await patientsService.list(); const currentPatients = filteredPatients.slice(startIndex, endIndex);
const mapped = res.map((p: any) => ({
id: String(p.id ?? ""),
nome: p.full_name ?? "",
telefone: p.phone_mobile ?? p.phone1 ?? "",
cidade: p.city ?? "",
estado: p.state ?? "",
ultimoAtendimento: p.last_visit_at ?? "",
proximoAtendimento: p.next_appointment_at ?? "",
vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "",
status: p.status ?? undefined,
}));
setPatients(mapped);
setCurrentPage(1); // Resetar para a primeira página ao carregar
} catch (e: any) {
setError(e?.message || "Erro ao buscar pacientes");
setPatients([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { // --- ESTADOS DE DIALOGS ---
fetchPacientes(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
}, [fetchPacientes]); const [patientToDelete, setPatientToDelete] = useState<string | null>(null);
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [patientDetails, setPatientDetails] = useState<any | null>(null);
const handleDeletePatient = async (patientId: string) => { // --- FUNÇÕES DE LÓGICA ---
try {
await patientsService.delete(patientId);
// Recarrega a lista para refletir a exclusão
await fetchPacientes();
} catch (e: any) {
setError(e?.message || "Erro ao deletar paciente");
}
setDeleteDialogOpen(false);
setPatientToDelete(null);
};
const openDeleteDialog = (patientId: string) => { // 1. Função para carregar TODOS os pacientes da API
setPatientToDelete(patientId); const fetchAllPacientes = useCallback(
setDeleteDialogOpen(true); async () => {
}; setLoading(true);
setError(null);
try {
// Como o backend retorna um array, chamamos sem paginação
const res = await patientsService.list();
const filteredPatients = patients.filter((patient) => { const mapped = res.map((p: any) => ({
const matchesSearch = id: String(p.id ?? ""),
patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) || nome: p.full_name ?? "—",
patient.telefone?.includes(searchTerm); telefone: p.phone_mobile ?? p.phone1 ?? "—",
const matchesConvenio = cidade: p.city ?? "—",
convenioFilter === "all" || (patient.convenio ?? "") === convenioFilter; estado: p.state ?? "—",
const matchesVip = // Formate as datas se necessário, aqui usamos como string
vipFilter === "all" || ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
(vipFilter === "vip" && patient.vip) || proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—",
(vipFilter === "regular" && !patient.vip); vip: Boolean(p.vip ?? false),
convenio: p.convenio ?? "Particular", // Define um valor padrão
status: p.status ?? undefined,
}));
return matchesSearch && matchesConvenio && matchesVip; setAllPatients(mapped);
}); } catch (e: any) {
console.error(e);
setError(e?.message || "Erro ao buscar pacientes");
} finally {
setLoading(false);
}
},
[]
);
// --- LÓGICA DE PAGINAÇÃO (ADICIONADA) --- // 2. Efeito para aplicar filtros e calcular a lista filtrada (chama-se quando allPatients ou filtros mudam)
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage); useEffect(() => {
const indexOfLastItem = currentPage * itemsPerPage; const filtered = allPatients.filter((patient) => {
const indexOfFirstItem = indexOfLastItem - itemsPerPage; // Filtro por termo de busca (Nome ou Telefone)
const currentItems = filteredPatients.slice(indexOfFirstItem, indexOfLastItem); const matchesSearch =
const paginate = (pageNumber: number) => setCurrentPage(pageNumber); patient.nome?.toLowerCase().includes(searchTerm.toLowerCase()) ||
patient.telefone?.includes(searchTerm);
const goToPrevPage = () => { // Filtro por Convênio
setCurrentPage((prev) => Math.max(1, prev - 1)); const matchesConvenio =
}; convenioFilter === "all" ||
const goToNextPage = () => { patient.convenio === convenioFilter;
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
};
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { // Filtro por VIP
const pages: number[] = []; const matchesVip =
const maxVisiblePages = 5; vipFilter === "all" ||
const halfRange = Math.floor(maxVisiblePages / 2); (vipFilter === "vip" && patient.vip) ||
let startPage = Math.max(1, currentPage - halfRange); (vipFilter === "regular" && !patient.vip);
let endPage = Math.min(totalPages, currentPage + halfRange);
if (endPage - startPage + 1 < maxVisiblePages) { return matchesSearch && matchesConvenio && matchesVip;
if (endPage === totalPages) { });
startPage = Math.max(1, totalPages - maxVisiblePages + 1);
}
if (startPage === 1) {
endPage = Math.min(totalPages, maxVisiblePages);
}
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages;
};
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); setFilteredPatients(filtered);
// Garante que a página atual seja válida após a filtragem
setPage(1);
}, [allPatients, searchTerm, convenioFilter, vipFilter]);
const handleItemsPerPageChange = (value: string) => { // 3. Efeito inicial para buscar os pacientes
setItemsPerPage(Number(value)); useEffect(() => {
setCurrentPage(1); // Resetar para a primeira página fetchAllPacientes();
}; // eslint-disable-next-line react-hooks/exhaustive-deps
// --------------------------------------------- }, []);
return ( // --- LÓGICA DE AÇÕES (DELETAR / VER DETALHES) ---
<ManagerLayout>
<div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-xl md:text-2xl font-bold text-foreground">
Pacientes
</h1>
<p className="text-muted-foreground text-sm md:text-base">
Gerencie as informações de seus pacientes
</p>
</div>
<div className="flex gap-2">
<Link href="/manager/pacientes/novo">
<Button className="w-full md:w-auto bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
Adicionar Novo
</Button>
</Link>
</div>
</div>
<div className="flex flex-col md:flex-row flex-wrap gap-4 bg-card p-4 rounded-lg border border-border"> const openDetailsDialog = async (patientId: string) => {
<div className="flex items-center gap-2 w-full md:w-auto"> setDetailsDialogOpen(true);
<span className="text-sm font-medium text-foreground"> setPatientDetails(null);
Convênio try {
</span> const res = await patientsService.getById(patientId);
<Select value={convenioFilter} onValueChange={setConvenioFilter}> setPatientDetails(Array.isArray(res) ? res[0] : res); // Supondo que retorne um array com um item
<SelectTrigger className="w-full md:w-40"> } catch (e: any) {
<SelectValue placeholder="Selecione o Convênio" /> setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
</SelectTrigger> }
<SelectContent> };
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="Particular">Particular</SelectItem>
<SelectItem value="SUS">SUS</SelectItem>
<SelectItem value="Unimed">Unimed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2 w-full md:w-auto"> const handleDeletePatient = async (patientId: string) => {
<span className="text-sm font-medium text-foreground">VIP</span> try {
<Select value={vipFilter} onValueChange={setVipFilter}> await patientsService.delete(patientId);
<SelectTrigger className="w-full md:w-32"> // Atualiza a lista completa para refletir a exclusão
<SelectValue placeholder="Selecione" /> setAllPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId)));
</SelectTrigger> } catch (e: any) {
<SelectContent> alert(`Erro ao deletar paciente: ${e?.message || 'Erro desconhecido'}`);
<SelectItem value="all">Todos</SelectItem> }
<SelectItem value="vip">VIP</SelectItem> setDeleteDialogOpen(false);
<SelectItem value="regular">Regular</SelectItem> setPatientToDelete(null);
</SelectContent> };
</Select>
</div>
{/* SELETOR DE ITENS POR PÁGINA (ADICIONADO) */} const openDeleteDialog = (patientId: string) => {
<div className="flex items-center gap-2 w-full md:w-auto"> setPatientToDelete(patientId);
<span className="text-sm font-medium text-foreground">Itens por página</span> setDeleteDialogOpen(true);
<Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}> };
<SelectTrigger className="w-full md:w-32">
<SelectValue placeholder="Itens por pág." />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5 por página</SelectItem>
<SelectItem value="10">10 por página</SelectItem>
<SelectItem value="20">20 por página</SelectItem>
</SelectContent>
</Select>
</div>
<Button variant="outline" className="ml-auto w-full md:w-auto"> return (
<Filter className="w-4 h-4 mr-2" /> <ManagerLayout>
Filtro avançado <div className="space-y-6 px-2 sm:px-4 md:px-8">
</Button> {/* Header (Responsividade OK) */}
</div> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div>
<h1 className="text-xl md:text-2xl font-bold text-foreground">Pacientes</h1>
<p className="text-muted-foreground text-sm md:text-base">Gerencie as informações de seus pacientes</p>
</div>
</div>
<div className="bg-white rounded-lg border border-gray-200"> {/* Bloco de Filtros (Responsividade APLICADA) */}
<div className="overflow-x-auto"> <div className="flex flex-wrap items-center gap-4 bg-card p-4 rounded-lg border border-border">
{loading ? ( <Filter className="w-5 h-5 text-gray-400" />
<div className="p-8 text-center text-gray-500">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" /> {/* Busca - Ocupa 100% no mobile, depois cresce */}
Carregando pacientes... <input
</div> type="text"
) : error ? ( placeholder="Buscar por nome ou telefone..."
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div> value={searchTerm}
) : ( onChange={(e) => setSearchTerm(e.target.value)}
<table className="w-full min-w-[600px]"> className="w-full sm:flex-grow sm:min-w-[150px] p-2 border rounded-md text-sm"
<thead className="bg-gray-50 border-b border-gray-200"> />
<tr>
<th className="text-left p-4 font-medium text-gray-700">Nome</th> {/* Convênio - Ocupa metade da linha no mobile */}
<th className="text-left p-4 font-medium text-gray-700">Telefone</th> <div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[200px]">
<th className="text-left p-4 font-medium text-gray-700">Cidade</th> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">Convênio</span>
<th className="text-left p-4 font-medium text-gray-700">Último Atendimento</th> <Select value={convenioFilter} onValueChange={setConvenioFilter}>
<th className="text-right p-4 font-medium text-gray-700">Ações</th> <SelectTrigger className="w-full sm:w-40">
</tr> <SelectValue placeholder="Convênio" />
</thead> </SelectTrigger>
<tbody> <SelectContent>
{filteredPatients.length === 0 ? ( <SelectItem value="all">Todos</SelectItem>
<tr> <SelectItem value="Particular">Particular</SelectItem>
<td colSpan={5} className="p-8 text-center text-gray-500"> <SelectItem value="SUS">SUS</SelectItem>
{patients.length === 0 ? "Nenhum paciente cadastrado." : "Nenhum paciente encontrado com os filtros aplicados."} <SelectItem value="Unimed">Unimed</SelectItem>
</td> {/* Adicione outros convênios conforme necessário */}
</tr> </SelectContent>
) : ( </Select>
// Mapeando `currentItems` em vez de `filteredPatients` </div>
currentItems.map((patient) => (
<tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50"> {/* VIP - Ocupa a outra metade da linha no mobile */}
<td className="p-4"> <div className="flex items-center gap-2 w-[calc(50%-8px)] sm:w-auto sm:flex-grow sm:max-w-[150px]">
<div className="flex items-center gap-3"> <span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
<div className="w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center"> <Select value={vipFilter} onValueChange={setVipFilter}>
<span className="text-gray-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span> <SelectTrigger className="w-full sm:w-32">
<SelectValue placeholder="VIP" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="vip">VIP</SelectItem>
<SelectItem value="regular">Regular</SelectItem>
</SelectContent>
</Select>
</div>
{/* Aniversariantes - Vai para a linha de baixo no mobile, ocupando 100% */}
<Button variant="outline" className="w-full md:w-auto md:ml-auto">
<Calendar className="w-4 h-4 mr-2" />
Aniversariantes
</Button>
</div>
{/* Tabela (Responsividade APLICADA) */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md">
<div className="overflow-x-auto">
{error ? (
<div className="p-6 text-red-600">{`Erro ao carregar pacientes: ${error}`}</div>
) : 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> </div>
<span className="font-medium text-gray-900">{patient.nome}</span> ) : (
</div> // min-w ajustado para responsividade
</td> <table className="w-full min-w-[650px] md:min-w-[900px]">
<td className="p-4 text-gray-600">{patient.telefone}</td> <thead className="bg-gray-50 border-b border-gray-200">
<td className="p-4 text-gray-600">{patient.cidade}</td> <tr>
<td className="p-4 text-gray-600">{formatDate(patient.ultimoAtendimento)}</td> <th className="text-left p-4 font-medium text-gray-700 w-[20%]">Nome</th>
<td className="p-4 text-right"> {/* Coluna oculta em telas muito pequenas */}
<DropdownMenu> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Telefone</th>
<DropdownMenuTrigger asChild> {/* Coluna oculta em telas pequenas e muito pequenas */}
<div className="text-blue-600 cursor-pointer inline-block">Ações</div> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden md:table-cell">Cidade / Estado</th>
</DropdownMenuTrigger> {/* Coluna oculta em telas muito pequenas */}
<DropdownMenuContent align="end"> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden sm:table-cell">Convênio</th>
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}> {/* Colunas ocultas em telas médias, pequenas e muito pequenas */}
<Eye className="w-4 h-4 mr-2" /> <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Último atendimento</th>
Ver detalhes <th className="text-left p-4 font-medium text-gray-700 w-[15%] hidden lg:table-cell">Próximo atendimento</th>
</DropdownMenuItem> <th className="text-left p-4 font-medium text-gray-700 w-[5%]">Ações</th>
<DropdownMenuItem asChild> </tr>
<Link href={`/manager/pacientes/${patient.id}/editar`}> </thead>
<Edit className="w-4 h-4 mr-2" /> <tbody>
Editar {currentPatients.length === 0 ? (
</Link> <tr>
</DropdownMenuItem> <td colSpan={7} className="p-8 text-center text-gray-500">
<DropdownMenuItem> {allPatients.length === 0 ? "Nenhum paciente cadastrado" : "Nenhum paciente encontrado com os filtros aplicados"}
<Calendar className="w-4 h-4 mr-2" /> </td>
Marcar consulta </tr>
</DropdownMenuItem> ) : (
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}> currentPatients.map((patient) => (
<Trash2 className="w-4 h-4 mr-2" /> <tr key={patient.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="p-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-green-600 font-medium text-sm">{patient.nome?.charAt(0) || "?"}</span>
</div>
<span className="font-medium text-gray-900">
{patient.nome}
{patient.vip && (
<span className="ml-2 px-2 py-0.5 text-xs font-semibold text-purple-600 bg-purple-100 rounded-full">VIP</span>
)}
</span>
</div>
</td>
{/* Aplicação das classes de visibilidade */}
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.telefone}</td>
<td className="p-4 text-gray-600 hidden md:table-cell">{`${patient.cidade} / ${patient.estado}`}</td>
<td className="p-4 text-gray-600 hidden sm:table-cell">{patient.convenio}</td>
<td className="p-4 text-gray-600 hidden lg:table-cell">{patient.ultimoAtendimento}</td>
<td className="p-4 text-gray-600 hidden lg:table-cell">{patient.proximoAtendimento}</td>
<td className="p-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="text-blue-600 cursor-pointer">Ações</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => openDetailsDialog(String(patient.id))}>
<Eye className="w-4 h-4 mr-2" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
<Edit className="w-4 h-4 mr-2" />
Editar
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Calendar className="w-4 h-4 mr-2" />
Marcar consulta
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
<Trash2 className="w-4 h-4 mr-2" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</td>
</tr>
))
)}
</tbody>
</table>
)}
</div>
{/* Paginação */}
{totalPages > 1 && !loading && (
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
{/* Renderização dos botões de número de página (Limitando a 5) */}
<div className="flex space-x-2"> {/* Increased space-x for more separation */}
{/* Botão Anterior */}
<Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
&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" // Changed to "lg" for larger buttons
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
>
{pageNumber}
</Button>
))}
{/* Botão Próximo */}
<Button
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages}
variant="outline"
size="lg" // Changed to "lg" for larger buttons
>
Próximo &gt;
</Button>
</div>
</div>
)}
</div>
{/* AlertDialogs (Permanecem os mesmos) */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
{/* ... (AlertDialog de Exclusão) ... */}
<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
</DropdownMenuItem> </AlertDialogAction>
</DropdownMenuContent> </AlertDialogFooter>
</DropdownMenu> </AlertDialogContent>
</td> </AlertDialog>
</tr>
))
)}
</tbody>
</table>
)}
</div>
</div>
{/* COMPONENTE DE PAGINAÇÃO (ADICIONADO) */} <AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
{totalPages > 1 && ( {/* ... (AlertDialog de Detalhes) ... */}
<div className="flex flex-wrap justify-center items-center gap-2 mt-4 p-4 bg-white rounded-lg border border-gray-200 shadow-md"> <AlertDialogContent>
<button <AlertDialogHeader>
onClick={goToPrevPage} <AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle>
disabled={currentPage === 1} <AlertDialogDescription>
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300" {patientDetails === null ? (
> <div className="text-gray-500">
{"< Anterior"} <Loader2 className="w-6 h-6 animate-spin mx-auto text-green-600 my-4" />
</button> Carregando...
</div>
{visiblePageNumbers.map((number) => ( ) : patientDetails?.error ? (
<button <div className="text-red-600 p-4">{patientDetails.error}</div>
key={number} ) : (
onClick={() => paginate(number)} <div className="grid gap-4 py-4">
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-gray-300 ${currentPage === number <div className="grid grid-cols-2 gap-4">
? "bg-green-600 text-white shadow-md border-green-600" <div>
: "bg-gray-100 text-gray-700 hover:bg-gray-200" <p className="font-semibold">Nome Completo</p>
}`} <p>{patientDetails.full_name}</p>
> </div>
{number} <div>
</button> <p className="font-semibold">Email</p>
))} <p>{patientDetails.email}</p>
</div>
<button <div>
onClick={goToNextPage} <p className="font-semibold">Telefone</p>
disabled={currentPage === totalPages} <p>{patientDetails.phone_mobile}</p>
className="flex items-center px-4 py-2 rounded-md font-medium transition-colors text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed border border-gray-300" </div>
> <div>
{"Próximo >"} <p className="font-semibold">Data de Nascimento</p>
</button> <p>{patientDetails.birth_date}</p>
</div> </div>
)} <div>
<p className="font-semibold">CPF</p>
{/* MODAIS (SEM ALTERAÇÃO) */} <p>{patientDetails.cpf}</p>
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> </div>
<AlertDialogContent> <div>
<AlertDialogHeader> <p className="font-semibold">Tipo Sanguíneo</p>
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle> <p>{patientDetails.blood_type}</p>
<AlertDialogDescription> </div>
Tem certeza que deseja excluir este paciente? Esta ação não pode <div>
ser desfeita. <p className="font-semibold">Peso (kg)</p>
</AlertDialogDescription> <p>{patientDetails.weight_kg}</p>
</AlertDialogHeader> </div>
<AlertDialogFooter> <div>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <p className="font-semibold">Altura (m)</p>
<AlertDialogAction <p>{patientDetails.height_m}</p>
onClick={() => </div>
patientToDelete && handleDeletePatient(patientToDelete) </div>
} <div className="border-t pt-4 mt-4">
className="bg-red-600 hover:bg-red-700" <h3 className="font-semibold mb-2">Endereço</h3>
> <div className="grid grid-cols-2 gap-4">
Excluir <div>
</AlertDialogAction> <p className="font-semibold">Rua</p>
</AlertDialogFooter> <p>{`${patientDetails.street}, ${patientDetails.number}`}</p>
</AlertDialogContent> </div>
</AlertDialog> <div>
<p className="font-semibold">Complemento</p>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}> <p>{patientDetails.complement}</p>
<AlertDialogContent> </div>
<AlertDialogHeader> <div>
<AlertDialogTitle>Detalhes do Paciente</AlertDialogTitle> <p className="font-semibold">Bairro</p>
<AlertDialogDescription> <p>{patientDetails.neighborhood}</p>
{patientDetails === null ? ( </div>
<div className="text-gray-500">Carregando...</div> <div>
) : patientDetails?.error ? ( <p className="font-semibold">Cidade</p>
<div className="text-red-600">{patientDetails.error}</div> <p>{patientDetails.cidade}</p>
) : ( </div>
<div className="space-y-2 text-left"> <div>
<p><strong>Nome:</strong> {patientDetails.full_name}</p> <p className="font-semibold">Estado</p>
<p><strong>CPF:</strong> {patientDetails.cpf}</p> <p>{patientDetails.estado}</p>
<p><strong>Email:</strong> {patientDetails.email}</p> </div>
<p><strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}</p> <div>
<p><strong>Endereço:</strong> {`${patientDetails.street ?? "-"}, ${patientDetails.neighborhood ?? "-"}, ${patientDetails.city ?? "-"} - ${patientDetails.state ?? "-"}`}</p> <p className="font-semibold">CEP</p>
<p><strong>Criado em:</strong> {formatDate(patientDetails.created_at)}</p> <p>{patientDetails.cep}</p>
</div> </div>
)} </div>
</AlertDialogDescription> </div>
</AlertDialogHeader> </div>
<AlertDialogFooter> )}
<AlertDialogCancel>Fechar</AlertDialogCancel> </AlertDialogDescription>
</AlertDialogFooter> </AlertDialogHeader>
</AlertDialogContent> <AlertDialogFooter>
</AlertDialog> <AlertDialogCancel>Fechar</AlertDialogCancel>
</div> </AlertDialogFooter>
</ManagerLayout> </AlertDialogContent>
); </AlertDialog>
</div>
</ManagerLayout>
);
} }

View File

@ -311,31 +311,27 @@ export default function PacientesPage() {
{/* Paginação */} {/* Paginação */}
{totalPages > 1 && !loading && ( {totalPages > 1 && !loading && (
<div className="flex flex-wrap items-center justify-between 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">
{/* Adicionado contador de página para melhor informação no mobile */} {/* Renderização dos botões de número de página (Limitando a 5) */}
<div className="flex space-x-2"> {/* Increased space-x for more separation */}
{/* Botões de Navegação */}
<div className="flex space-x-1 order-1 w-full justify-center sm:w-auto sm:justify-start sm:order-2">
{/* Botão Anterior */} {/* Botão Anterior */}
<Button <Button
onClick={() => setPage((prev) => Math.max(1, prev - 1))} onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1} disabled={page === 1}
variant="outline" variant="outline"
size="sm" size="lg" // Changed to "lg" for larger buttons
> >
&lt; Anterior &lt; Anterior
</Button> </Button>
{/* Renderização dos botões de número de página (Limitando a 5) */}
{Array.from({ length: totalPages }, (_, index) => index + 1) {Array.from({ length: totalPages }, (_, index) => index + 1)
// Limita a exibição dos botões para 5 (janela de visualização)
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2)) .slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
.map((pageNumber) => ( .map((pageNumber) => (
<Button <Button
key={pageNumber} key={pageNumber}
onClick={() => setPage(pageNumber)} onClick={() => setPage(pageNumber)}
variant={pageNumber === page ? "default" : "outline"} variant={pageNumber === page ? "default" : "outline"}
size="sm" size="lg" // Changed to "lg" for larger buttons
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"} className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
> >
{pageNumber} {pageNumber}
@ -347,7 +343,7 @@ export default function PacientesPage() {
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))} onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
disabled={page === totalPages} disabled={page === totalPages}
variant="outline" variant="outline"
size="sm" size="lg" // Changed to "lg" for larger buttons
> >
Próximo &gt; Próximo &gt;
</Button> </Button>

View File

@ -145,12 +145,6 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
label: "Consultas", label: "Consultas",
// Botão para página de consultas marcadas do médico atual // Botão para página de consultas marcadas do médico atual
}, },
{
href: "/doctor/medicos/editorlaudo",
icon: Clock,
label: "Editor de Laudo",
// Botão para página do editor de laudo
},
{ {
href: "/doctor/medicos", href: "/doctor/medicos",
icon: User, icon: User,