805 lines
31 KiB
TypeScript
805 lines
31 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
import toast from "react-hot-toast";
|
||
import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react";
|
||
import { patientService, type Patient } from "../../services";
|
||
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
||
import { Avatar } from "../ui/Avatar";
|
||
import { useAuth } from "../../hooks/useAuth";
|
||
|
||
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
||
|
||
const CONVENIOS = [
|
||
"Particular",
|
||
"Unimed",
|
||
"Amil",
|
||
"Bradesco Saúde",
|
||
"SulAmérica",
|
||
"Golden Cross",
|
||
];
|
||
|
||
const COUNTRY_OPTIONS = [
|
||
{ value: "55", label: "+55 🇧🇷 Brasil" },
|
||
{ value: "1", label: "+1 🇺🇸 EUA/Canadá" },
|
||
];
|
||
|
||
// Função para buscar endereço via CEP
|
||
const buscarEnderecoViaCEP = async (cep: string) => {
|
||
try {
|
||
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
|
||
const data = await response.json();
|
||
if (data.erro) return null;
|
||
return {
|
||
rua: data.logradouro,
|
||
bairro: data.bairro,
|
||
cidade: data.localidade,
|
||
estado: data.uf,
|
||
cep: data.cep,
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
};
|
||
|
||
export function SecretaryPatientList({
|
||
onOpenAppointment,
|
||
}: {
|
||
onOpenAppointment?: (patientId: string) => void;
|
||
}) {
|
||
const { user } = useAuth();
|
||
const [patients, setPatients] = useState<Patient[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [searchTerm, setSearchTerm] = useState("");
|
||
const [insuranceFilter, setInsuranceFilter] = useState("Todos");
|
||
const [showBirthdays, setShowBirthdays] = useState(false);
|
||
const [showVIP, setShowVIP] = useState(false);
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [itemsPerPage] = useState(10);
|
||
|
||
// Modal states
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||
const [patientToDelete, setPatientToDelete] = useState<Patient | null>(null);
|
||
const [showViewModal, setShowViewModal] = useState(false);
|
||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||
const [formData, setFormData] = useState<PacienteFormData>({
|
||
nome: "",
|
||
social_name: "",
|
||
cpf: "",
|
||
sexo: "",
|
||
dataNascimento: "",
|
||
email: "",
|
||
codigoPais: "55",
|
||
ddd: "",
|
||
numeroTelefone: "",
|
||
tipo_sanguineo: "",
|
||
altura: "",
|
||
peso: "",
|
||
convenio: "Particular",
|
||
numeroCarteirinha: "",
|
||
observacoes: "",
|
||
endereco: {
|
||
cep: "",
|
||
rua: "",
|
||
numero: "",
|
||
bairro: "",
|
||
cidade: "",
|
||
estado: "",
|
||
},
|
||
});
|
||
const [cpfError, setCpfError] = useState<string | null>(null);
|
||
const [cpfValidationMessage, setCpfValidationMessage] = useState<
|
||
string | null
|
||
>(null);
|
||
|
||
const loadPatients = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await patientService.list();
|
||
console.log("✅ Pacientes carregados:", data);
|
||
// Log para verificar se temos user_id
|
||
if (Array.isArray(data) && data.length > 0) {
|
||
console.log("📋 Primeiro paciente (verificar user_id):", {
|
||
full_name: data[0].full_name,
|
||
user_id: data[0].user_id,
|
||
avatar_url: data[0].avatar_url,
|
||
email: data[0].email,
|
||
});
|
||
}
|
||
setPatients(Array.isArray(data) ? data : []);
|
||
if (Array.isArray(data) && data.length === 0) {
|
||
console.warn("⚠️ Nenhum paciente encontrado na API");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ Erro ao carregar pacientes:", error);
|
||
toast.error("Erro ao carregar pacientes");
|
||
setPatients([]);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadPatients();
|
||
}, []);
|
||
|
||
// Função de filtro
|
||
const filteredPatients = patients.filter((patient) => {
|
||
// Filtro de busca por nome, CPF ou email
|
||
const searchLower = searchTerm.toLowerCase();
|
||
const matchesSearch =
|
||
!searchTerm ||
|
||
patient.full_name?.toLowerCase().includes(searchLower) ||
|
||
patient.cpf?.includes(searchTerm) ||
|
||
patient.email?.toLowerCase().includes(searchLower);
|
||
|
||
// Filtro de aniversariantes do mês
|
||
const matchesBirthday =
|
||
!showBirthdays ||
|
||
(() => {
|
||
if (!patient.birth_date) return false;
|
||
const birthDate = new Date(patient.birth_date);
|
||
const currentMonth = new Date().getMonth();
|
||
const birthMonth = birthDate.getMonth();
|
||
return currentMonth === birthMonth;
|
||
})();
|
||
|
||
// Filtro de convênio
|
||
const matchesInsurance =
|
||
insuranceFilter === "Todos" ||
|
||
((patient as any).convenio || "Particular") === insuranceFilter;
|
||
|
||
// Filtro VIP (se o backend fornecer uma flag 'is_vip' ou 'vip')
|
||
const matchesVIP = !showVIP || ((patient as any).is_vip === true || (patient as any).vip === true);
|
||
|
||
return matchesSearch && matchesBirthday && matchesInsurance && matchesVIP;
|
||
});
|
||
|
||
// Cálculos de paginação
|
||
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage);
|
||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||
const endIndex = startIndex + itemsPerPage;
|
||
const paginatedPatients = filteredPatients.slice(startIndex, endIndex);
|
||
|
||
const handleSearch = () => {
|
||
loadPatients();
|
||
};
|
||
|
||
const handleClear = () => {
|
||
setSearchTerm("");
|
||
setInsuranceFilter("Todos");
|
||
setShowBirthdays(false);
|
||
setShowVIP(false);
|
||
setCurrentPage(1);
|
||
loadPatients();
|
||
};
|
||
|
||
// Reset página quando filtros mudarem
|
||
useEffect(() => {
|
||
setCurrentPage(1);
|
||
}, [searchTerm, insuranceFilter, showBirthdays, showVIP]);
|
||
|
||
const handleNewPatient = () => {
|
||
setModalMode("create");
|
||
setFormData({
|
||
nome: "",
|
||
social_name: "",
|
||
cpf: "",
|
||
sexo: "",
|
||
dataNascimento: "",
|
||
email: "",
|
||
codigoPais: "55",
|
||
ddd: "",
|
||
numeroTelefone: "",
|
||
tipo_sanguineo: "",
|
||
altura: "",
|
||
peso: "",
|
||
convenio: "Particular",
|
||
numeroCarteirinha: "",
|
||
observacoes: "",
|
||
endereco: {
|
||
cep: "",
|
||
rua: "",
|
||
numero: "",
|
||
bairro: "",
|
||
cidade: "",
|
||
estado: "",
|
||
},
|
||
});
|
||
setCpfError(null);
|
||
setCpfValidationMessage(null);
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleEditPatient = (patient: Patient) => {
|
||
setModalMode("edit");
|
||
setFormData({
|
||
id: patient.id,
|
||
user_id: patient.user_id,
|
||
nome: patient.full_name || "",
|
||
social_name: patient.social_name || "",
|
||
cpf: patient.cpf || "",
|
||
sexo: patient.sex || "",
|
||
dataNascimento: patient.birth_date || "",
|
||
email: patient.email || "",
|
||
codigoPais: "55",
|
||
ddd: "",
|
||
numeroTelefone: patient.phone_mobile || "",
|
||
tipo_sanguineo: patient.blood_type || "",
|
||
altura: patient.height_m?.toString() || "",
|
||
peso: patient.weight_kg?.toString() || "",
|
||
convenio: "Particular",
|
||
numeroCarteirinha: "",
|
||
observacoes: "",
|
||
avatar_url: patient.avatar_url || undefined,
|
||
endereco: {
|
||
cep: patient.cep || "",
|
||
rua: patient.street || "",
|
||
numero: patient.number || "",
|
||
complemento: patient.complement || "",
|
||
bairro: patient.neighborhood || "",
|
||
cidade: patient.city || "",
|
||
estado: patient.state || "",
|
||
},
|
||
});
|
||
setCpfError(null);
|
||
setCpfValidationMessage(null);
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleFormChange = (patch: Partial<PacienteFormData>) => {
|
||
setFormData((prev) => ({ ...prev, ...patch }));
|
||
};
|
||
|
||
const handleCpfChange = (value: string) => {
|
||
setFormData((prev) => ({ ...prev, cpf: value }));
|
||
setCpfError(null);
|
||
setCpfValidationMessage(null);
|
||
};
|
||
|
||
const handleCepLookup = async (cep: string) => {
|
||
const endereco = await buscarEnderecoViaCEP(cep);
|
||
if (endereco) {
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
endereco: {
|
||
...prev.endereco,
|
||
...endereco,
|
||
},
|
||
}));
|
||
toast.success("Endereço encontrado!");
|
||
} else {
|
||
toast.error("CEP não encontrado");
|
||
}
|
||
};
|
||
|
||
const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||
e.preventDefault();
|
||
setLoading(true);
|
||
|
||
try {
|
||
if (modalMode === "edit" && formData.id) {
|
||
// Para edição, usa o endpoint antigo (PATCH /patients/:id)
|
||
// Remove formatação de telefone, CPF e CEP
|
||
const cleanPhone = formData.numeroTelefone.replace(/\D/g, "");
|
||
const cleanCpf = formData.cpf.replace(/\D/g, "");
|
||
const cleanCep = formData.endereco.cep
|
||
? formData.endereco.cep.replace(/\D/g, "")
|
||
: null;
|
||
|
||
const patientData = {
|
||
full_name: formData.nome,
|
||
social_name: formData.social_name || null,
|
||
cpf: cleanCpf,
|
||
sex: formData.sexo || null,
|
||
birth_date: formData.dataNascimento || null,
|
||
email: formData.email,
|
||
phone_mobile: cleanPhone,
|
||
blood_type: formData.tipo_sanguineo || null,
|
||
height_m: formData.altura ? parseFloat(formData.altura) : null,
|
||
weight_kg: formData.peso ? parseFloat(formData.peso) : null,
|
||
cep: cleanCep,
|
||
street: formData.endereco.rua || null,
|
||
number: formData.endereco.numero || null,
|
||
complement: formData.endereco.complemento || null,
|
||
neighborhood: formData.endereco.bairro || null,
|
||
city: formData.endereco.cidade || null,
|
||
state: formData.endereco.estado || null,
|
||
};
|
||
await patientService.update(formData.id, patientData);
|
||
toast.success("Paciente atualizado com sucesso!");
|
||
} else {
|
||
// Criar novo paciente usando a API REST direta
|
||
// Remove formatação de telefone e CPF
|
||
const cleanPhone = formData.numeroTelefone.replace(/\D/g, "");
|
||
const cleanCpf = formData.cpf.replace(/\D/g, "");
|
||
const cleanCep = formData.endereco.cep
|
||
? formData.endereco.cep.replace(/\D/g, "")
|
||
: null;
|
||
|
||
const createData = {
|
||
full_name: formData.nome,
|
||
cpf: cleanCpf,
|
||
email: formData.email,
|
||
phone_mobile: cleanPhone,
|
||
birth_date: formData.dataNascimento || null,
|
||
social_name: formData.social_name || null,
|
||
sex: formData.sexo || null,
|
||
blood_type: formData.tipo_sanguineo || null,
|
||
weight_kg: formData.peso ? parseFloat(formData.peso) : null,
|
||
height_m: formData.altura ? parseFloat(formData.altura) : null,
|
||
street: formData.endereco.rua || null,
|
||
number: formData.endereco.numero || null,
|
||
complement: formData.endereco.complemento || null,
|
||
neighborhood: formData.endereco.bairro || null,
|
||
city: formData.endereco.cidade || null,
|
||
state: formData.endereco.estado || null,
|
||
cep: cleanCep,
|
||
created_by: user?.id || undefined,
|
||
};
|
||
|
||
await patientService.create(createData);
|
||
toast.success("Paciente cadastrado com sucesso!");
|
||
}
|
||
|
||
setShowModal(false);
|
||
loadPatients();
|
||
} catch (error) {
|
||
console.error("Erro ao salvar paciente:", error);
|
||
toast.error("Erro ao salvar paciente");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCancelForm = () => {
|
||
setShowModal(false);
|
||
};
|
||
|
||
const handleDeleteClick = (patient: Patient) => {
|
||
setPatientToDelete(patient);
|
||
setShowDeleteDialog(true);
|
||
};
|
||
|
||
const handleViewPatient = (patient: Patient) => {
|
||
setSelectedPatient(patient);
|
||
setShowViewModal(true);
|
||
};
|
||
|
||
const handleSchedulePatient = (patient: Patient) => {
|
||
if (onOpenAppointment) {
|
||
onOpenAppointment(patient.id as string);
|
||
} else {
|
||
// fallback: store in sessionStorage and dispatch event
|
||
sessionStorage.setItem("selectedPatientForAppointment", patient.id as string);
|
||
window.dispatchEvent(new CustomEvent("open-create-appointment"));
|
||
}
|
||
};
|
||
|
||
const handleConfirmDelete = async () => {
|
||
if (!patientToDelete?.id) return;
|
||
|
||
setLoading(true);
|
||
try {
|
||
await patientService.delete(patientToDelete.id);
|
||
toast.success("Paciente deletado com sucesso!");
|
||
setShowDeleteDialog(false);
|
||
setPatientToDelete(null);
|
||
loadPatients();
|
||
} catch (error) {
|
||
console.error("Erro ao deletar paciente:", error);
|
||
toast.error("Erro ao deletar paciente");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleCancelDelete = () => {
|
||
setShowDeleteDialog(false);
|
||
setPatientToDelete(null);
|
||
};
|
||
|
||
const getPatientColor = (
|
||
index: number
|
||
): "blue" | "green" | "purple" | "orange" | "pink" | "teal" => {
|
||
const colors: Array<
|
||
"blue" | "green" | "purple" | "orange" | "pink" | "teal"
|
||
> = ["blue", "green", "purple", "orange", "pink", "teal"];
|
||
return colors[index % colors.length];
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Pacientes</h1>
|
||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||
Gerencie os pacientes cadastrados
|
||
</p>
|
||
</div>
|
||
<button
|
||
onClick={handleNewPatient}
|
||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
Novo Paciente
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search and Filters */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
|
||
<div className="flex gap-3">
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
|
||
<input
|
||
type="text"
|
||
placeholder="Buscar pacientes por nome ou email..."
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
|
||
/>
|
||
</div>
|
||
<button
|
||
onClick={handleSearch}
|
||
className="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||
>
|
||
Buscar
|
||
</button>
|
||
<button
|
||
onClick={handleClear}
|
||
className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||
>
|
||
Limpar
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-6">
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={showBirthdays}
|
||
onChange={(e) => setShowBirthdays(e.target.checked)}
|
||
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
|
||
/>
|
||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||
Aniversariantes do mês
|
||
</span>
|
||
</label>
|
||
<label className="flex items-center gap-2 cursor-pointer">
|
||
<input
|
||
type="checkbox"
|
||
checked={showVIP}
|
||
onChange={(e) => setShowVIP(e.target.checked)}
|
||
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
|
||
/>
|
||
<span className="text-sm text-gray-700 dark:text-gray-300">Somente VIP</span>
|
||
</label>
|
||
<div className="flex items-center gap-2 ml-auto">
|
||
<span className="text-sm text-gray-600 dark:text-gray-400">Convênio:</span>
|
||
<select
|
||
value={insuranceFilter}
|
||
onChange={(e) => setInsuranceFilter(e.target.value)}
|
||
className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||
>
|
||
<option>Todos</option>
|
||
<option>Particular</option>
|
||
<option>Unimed</option>
|
||
<option>Amil</option>
|
||
<option>Bradesco Saúde</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Table */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||
<tr>
|
||
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||
Paciente
|
||
</th>
|
||
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||
Próximo Atendimento
|
||
</th>
|
||
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||
Convênio
|
||
</th>
|
||
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||
Ações
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||
{loading ? (
|
||
<tr>
|
||
<td
|
||
colSpan={4}
|
||
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
|
||
>
|
||
Carregando pacientes...
|
||
</td>
|
||
</tr>
|
||
) : filteredPatients.length === 0 ? (
|
||
<tr>
|
||
<td
|
||
colSpan={4}
|
||
className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
|
||
>
|
||
{searchTerm
|
||
? "Nenhum paciente encontrado com esse termo"
|
||
: "Nenhum paciente encontrado"}
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
paginatedPatients.map((patient, index) => (
|
||
<tr
|
||
key={patient.id}
|
||
className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||
>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-3">
|
||
<Avatar
|
||
src={patient}
|
||
name={patient.full_name || ""}
|
||
size="md"
|
||
color={getPatientColor(index)}
|
||
/>
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||
{patient.full_name}
|
||
</p>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">{patient.email}</p>
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||
{patient.phone_mobile}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||
{/* TODO: Buscar próximo agendamento */}—
|
||
</td>
|
||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||
{(patient as any).convenio || "Particular"}
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => handleViewPatient(patient)}
|
||
title="Visualizar"
|
||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||
>
|
||
<Eye className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleSchedulePatient(patient)}
|
||
title="Agendar consulta"
|
||
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
|
||
>
|
||
<Calendar className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleEditPatient(patient)}
|
||
title="Editar"
|
||
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteClick(patient)}
|
||
title="Deletar"
|
||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Paginação */}
|
||
{filteredPatients.length > 0 && (
|
||
<div className="flex items-center justify-between bg-white dark:bg-gray-800 px-6 py-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||
<div className="text-sm text-gray-700 dark:text-gray-300">
|
||
Mostrando {startIndex + 1} até {Math.min(endIndex, filteredPatients.length)} de {filteredPatients.length} pacientes
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||
disabled={currentPage === 1}
|
||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
|
||
>
|
||
Anterior
|
||
</button>
|
||
<div className="flex items-center gap-1">
|
||
{(() => {
|
||
const maxPagesToShow = 4;
|
||
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
|
||
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
|
||
|
||
if (endPage - startPage < maxPagesToShow - 1) {
|
||
startPage = Math.max(1, endPage - maxPagesToShow + 1);
|
||
}
|
||
|
||
const pages = [];
|
||
for (let i = startPage; i <= endPage; i++) {
|
||
pages.push(i);
|
||
}
|
||
|
||
return pages.map((page) => (
|
||
<button
|
||
key={page}
|
||
onClick={() => setCurrentPage(page)}
|
||
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
|
||
currentPage === page
|
||
? "bg-green-600 text-white"
|
||
: "border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
|
||
}`}
|
||
>
|
||
{page}
|
||
</button>
|
||
));
|
||
})()}
|
||
</div>
|
||
<button
|
||
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
||
disabled={currentPage === totalPages}
|
||
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
|
||
>
|
||
Próxima
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Modal de Formulário */}
|
||
{showModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||
<h2 className="text-xl font-semibold text-gray-900">
|
||
{modalMode === "create" ? "Novo Paciente" : "Editar Paciente"}
|
||
</h2>
|
||
<button
|
||
onClick={handleCancelForm}
|
||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Form Content */}
|
||
<div className="flex-1 overflow-y-auto p-6">
|
||
<PacienteForm
|
||
mode={modalMode}
|
||
loading={loading}
|
||
data={formData}
|
||
bloodTypes={BLOOD_TYPES}
|
||
convenios={CONVENIOS}
|
||
countryOptions={COUNTRY_OPTIONS}
|
||
cpfError={cpfError}
|
||
cpfValidationMessage={cpfValidationMessage}
|
||
onChange={handleFormChange}
|
||
onCpfChange={handleCpfChange}
|
||
onCepLookup={handleCepLookup}
|
||
onCancel={handleCancelForm}
|
||
onSubmit={handleFormSubmit}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Modal de Visualizar Paciente */}
|
||
{showViewModal && selectedPatient && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-xl shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||
<h2 className="text-xl font-semibold text-gray-900">Visualizar Paciente</h2>
|
||
<button
|
||
onClick={() => setShowViewModal(false)}
|
||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="p-6 space-y-4">
|
||
<div>
|
||
<p className="text-sm text-gray-500">Nome</p>
|
||
<p className="text-gray-900 font-medium">{selectedPatient.full_name}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">Email</p>
|
||
<p className="text-gray-900">{selectedPatient.email || '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">Telefone</p>
|
||
<p className="text-gray-900">{selectedPatient.phone_mobile || '—'}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-gray-500">Convênio</p>
|
||
<p className="text-gray-900">{(selectedPatient as any).convenio || 'Particular'}</p>
|
||
</div>
|
||
<div className="flex justify-end">
|
||
<button
|
||
onClick={() => setShowViewModal(false)}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
>
|
||
Fechar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
{showDeleteDialog && patientToDelete && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<div className="bg-white rounded-xl shadow-xl max-w-md w-full p-6">
|
||
<div className="flex items-start gap-4">
|
||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-100 flex items-center justify-center">
|
||
<Trash2 className="h-6 w-6 text-red-600" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||
Confirmar Exclusão
|
||
</h3>
|
||
<p className="text-sm text-gray-600 mb-4">
|
||
Tem certeza que deseja deletar o paciente{" "}
|
||
<span className="font-semibold">
|
||
{patientToDelete.full_name}
|
||
</span>
|
||
?
|
||
</p>
|
||
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
||
<h4 className="text-sm font-semibold text-red-900 mb-2">
|
||
⚠️ Atenção: Esta ação é irreversível
|
||
</h4>
|
||
<ul className="text-sm text-red-800 space-y-1">
|
||
<li>• Todos os dados do paciente serão perdidos</li>
|
||
<li>
|
||
• Histórico de consultas será mantido (por auditoria)
|
||
</li>
|
||
<li>
|
||
• Prontuários médicos serão mantidos (por legislação)
|
||
</li>
|
||
<li>• O paciente precisará se cadastrar novamente</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={handleCancelDelete}
|
||
disabled={loading}
|
||
className="flex-1 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
<button
|
||
onClick={handleConfirmDelete}
|
||
disabled={loading}
|
||
className="flex-1 px-4 py-2.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||
>
|
||
{loading ? "Deletando..." : "Sim, Deletar"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|