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>
</Select>
<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
</Button>
</Link>
@ -334,7 +334,7 @@ export default function PacientesPage() {
onClick={() => paginate(number)}
className={`px-4 py-2 rounded-md font-medium transition-colors text-sm border border-border ${
currentPage === number
? "bg-primary text-primary-foreground shadow-md border-primary"
? "bg-green-600 text-primary-foreground shadow-md border-green-600"
: "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>
<p className="text-sm text-gray-500">Gerencie todos os profissionais de saúde.</p>
</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>

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

View File

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

View File

@ -145,12 +145,6 @@ export default function DoctorLayout({ children }: PatientLayoutProps) {
label: "Consultas",
// 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",
icon: User,