2025-10-22 22:28:22 -03:00

1992 lines
77 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from "react";
import {
Users,
UserPlus,
Stethoscope,
UserCog,
Plus,
Edit,
Trash2,
Shield,
Search,
RefreshCw,
UserCheck,
UserX,
} from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { ConfirmDialog } from "../components/ui/ConfirmDialog";
import {
patientService,
type Patient,
doctorService,
type Doctor,
userService,
type UserInfo,
type UserRole,
type CreateUserInput,
profileService,
} from "../services";
import type { CrmUF } from "../services/doctors/types";
// Type aliases para compatibilidade
type Paciente = Patient;
type FullUserInfo = UserInfo;
type TabType = "pacientes" | "usuarios" | "medicos";
const PainelAdmin: React.FC = () => {
const { roles: authUserRoles } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<TabType>("pacientes");
const [loading, setLoading] = useState(false);
// Estados para pacientes
const [pacientes, setPacientes] = useState<Paciente[]>([]);
const [showPacienteModal, setShowPacienteModal] = useState(false);
const [editingPaciente, setEditingPaciente] = useState<Paciente | null>(null);
const [formPaciente, setFormPaciente] = useState({
full_name: "",
cpf: "",
email: "",
phone_mobile: "",
birth_date: "",
social_name: "",
sex: "",
blood_type: "",
weight_kg: "",
height_m: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
cep: "",
});
// Estados para usuários
const [usuarios, setUsuarios] = useState<FullUserInfo[]>([]);
const [searchTerm, setSearchTerm] = useState("");
const [showUserModal, setShowUserModal] = useState(false);
const [editingUser, setEditingUser] = useState<FullUserInfo | null>(null);
const [editForm, setEditForm] = useState<{
full_name?: string;
email?: string;
phone?: string;
disabled?: boolean;
}>({});
const [managingRolesUser, setManagingRolesUser] =
useState<FullUserInfo | null>(null);
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
const [newRole, setNewRole] = useState<UserRole>("user");
const [formUser, setFormUser] = useState<CreateUserInput>({
email: "",
full_name: "",
phone: "",
role: "user",
});
const [userPassword, setUserPassword] = useState(""); // Senha opcional
const [usePassword, setUsePassword] = useState(false); // Toggle para criar com senha
// Estados para dialog de confirmação
const [confirmDialog, setConfirmDialog] = useState<{
isOpen: boolean;
title: string;
message: string | React.ReactNode;
confirmText?: string;
cancelText?: string;
onConfirm: () => void;
requireTypedConfirmation: boolean;
confirmationWord: string;
isDangerous: boolean;
}>({
isOpen: false,
title: "",
message: "",
onConfirm: () => {},
requireTypedConfirmation: false,
confirmationWord: "",
isDangerous: false,
});
// Estados para médicos
const [medicos, setMedicos] = useState<Doctor[]>([]);
const [showMedicoModal, setShowMedicoModal] = useState(false);
const [editingMedico, setEditingMedico] = useState<Doctor | null>(null);
const [formMedico, setFormMedico] = useState({
crm: "",
crm_uf: "SP",
specialty: "",
full_name: "",
cpf: "",
email: "",
phone_mobile: "",
phone2: "",
cep: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
birth_date: "",
rg: "",
active: true,
});
// Verificar permissão de admin
useEffect(() => {
const isAdmin =
authUserRoles.includes("admin") || authUserRoles.includes("gestor");
if (!isAdmin) {
toast.error("Acesso negado: apenas administradores");
navigate("/");
}
}, [authUserRoles, navigate]);
// Carregar dados conforme aba ativa
useEffect(() => {
// Só carrega se não estiver já carregando
if (loading) return;
if (activeTab === "pacientes") {
loadPacientes();
} else if (activeTab === "usuarios") {
loadUsuarios();
} else if (activeTab === "medicos") {
loadMedicos();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeTab]);
const loadUsuarios = async () => {
setLoading(true);
try {
// Lista todos os perfis (usuários) do sistema
const profiles = await profileService.list();
// Busca todas as roles de uma vez
const allRoles = await userService.listRoles();
// Converte Profile para UserInfo (estrutura compatível com a interface esperada)
const userInfoList: FullUserInfo[] = profiles.map((profile) => {
// Filtra as roles deste usuário específico
const userRoles = allRoles
.filter((role) => role.user_id === profile.id)
.map((role) => role.role);
// Calcula permissões baseado nas roles
const isAdmin = userRoles.includes("admin");
const isManager = userRoles.includes("gestor");
const isDoctor = userRoles.includes("medico");
const isSecretary = userRoles.includes("secretaria");
return {
user: {
id: profile.id || "",
email: profile.email || "",
email_confirmed_at: null,
created_at: profile.created_at || new Date().toISOString(),
last_sign_in_at: null,
},
profile: {
id: profile.id || "",
full_name: profile.full_name,
email: profile.email,
phone: profile.phone || null,
avatar_url: profile.avatar_url,
disabled: profile.disabled || false,
created_at: profile.created_at || new Date().toISOString(),
updated_at: profile.updated_at || new Date().toISOString(),
},
roles: userRoles,
permissions: {
isAdmin,
isManager,
isDoctor,
isSecretary,
isAdminOrManager: isAdmin || isManager,
},
};
});
setUsuarios(userInfoList);
} catch (error) {
console.error("Erro ao carregar usuários:", error);
toast.error("Erro ao carregar usuários");
setUsuarios([]);
} finally {
setLoading(false);
}
};
const loadPacientes = async () => {
setLoading(true);
try {
const patients = await patientService.list();
setPacientes(patients);
} catch (error) {
console.error("Erro ao carregar pacientes:", error);
toast.error("Erro ao carregar pacientes");
} finally {
setLoading(false);
}
};
const loadMedicos = async () => {
setLoading(true);
try {
const doctors = await doctorService.list();
setMedicos(doctors);
} catch (error) {
console.error("Erro ao carregar médicos:", error);
toast.error("Erro ao carregar médicos");
} finally {
setLoading(false);
}
};
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Determina redirect_url baseado no role
let redirectUrl = "https://mediconnectbrasil.netlify.app/";
if (formUser.role === "medico") {
redirectUrl = "https://mediconnectbrasil.netlify.app/medico/painel";
} else if (formUser.role === "paciente") {
redirectUrl =
"https://mediconnectbrasil.netlify.app/paciente/agendamento";
} else if (formUser.role === "secretaria") {
redirectUrl = "https://mediconnectbrasil.netlify.app/secretaria/painel";
} else if (formUser.role === "admin" || formUser.role === "gestor") {
redirectUrl = "https://mediconnectbrasil.netlify.app/admin/painel";
}
// Criar com senha OU magic link
if (usePassword && userPassword.trim()) {
// Criar com senha
await userService.createUserWithPassword({
email: formUser.email,
password: userPassword,
full_name: formUser.full_name,
phone: formUser.phone,
role: formUser.role,
});
toast.success(
`Usuário ${formUser.full_name} criado com sucesso! Email de confirmação enviado.`
);
} else {
// Criar com magic link (padrão)
await userService.createUser(
{ ...formUser, redirect_url: redirectUrl },
false
);
toast.success(
`Usuário ${formUser.full_name} criado com sucesso! Magic link enviado para o email.`
);
}
setShowUserModal(false);
resetFormUser();
setUserPassword("");
setUsePassword(false);
loadUsuarios();
} catch (error) {
console.error("Erro ao criar usuário:", error);
toast.error("Erro ao criar usuário");
} finally {
setLoading(false);
}
};
// Funções de gerenciamento de usuários
// TODO: Implement admin user endpoints (update, enable/disable, delete)
const handleEditUser = (_user: FullUserInfo) => {
toast.error("Função de edição de usuário ainda não implementada");
// setEditingUser(user);
// setEditForm({
// full_name: user.profile?.full_name || "",
// email: user.profile?.email || "",
// phone: user.profile?.phone || "",
// disabled: user.profile?.disabled || false,
// });
};
const handleSaveEditUser = async () => {
toast.error("Função de salvar usuário ainda não implementada");
// TODO: Implement adminUserService.updateUser endpoint
// if (!editingUser) return;
// try {
// const result = await adminUserService.updateUser(editingUser.user.id, editForm);
// if (result.success) {
// toast.success("Usuário atualizado com sucesso!");
// setEditingUser(null);
// loadUsuarios();
// }
// } catch {
// toast.error("Erro ao atualizar usuário");
// }
};
const handleToggleStatusUser = async (
_userId: string,
_currentStatus: boolean
) => {
toast.error(
"Função de habilitar/desabilitar usuário ainda não implementada"
);
// TODO: Implement adminUserService.enableUser/disableUser endpoints
// try {
// const result = currentStatus
// ? await adminUserService.enableUser(userId)
// : await adminUserService.disableUser(userId);
// if (result.success) {
// toast.success(`Usuário ${currentStatus ? "habilitado" : "desabilitado"} com sucesso!`);
// loadUsuarios();
// }
// } catch {
// toast.error("Erro ao alterar status do usuário");
// }
};
const handleDeleteUser = async (userId: string, userName: string) => {
// Abre o dialog customizado de confirmação
setConfirmDialog({
isOpen: true,
title: "ATENÇÃO: OPERAÇÃO IRREVERSÍVEL!",
message: (
<div className="space-y-3">
<p className="font-medium">
Deseja deletar permanentemente o usuário{" "}
<span className="font-bold text-red-600">"{userName}"</span>?
</p>
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm font-semibold text-red-900 mb-2">
Isso irá deletar:
</p>
<ul className="text-sm text-red-800 space-y-1 list-disc list-inside">
<li>Conta de autenticação</li>
<li>Perfil do usuário</li>
<li>Todas as roles e permissões</li>
<li>Dados relacionados (cascata)</li>
</ul>
</div>
<p className="text-sm font-bold text-red-700">
Esta ação NÃO pode ser desfeita!
</p>
</div>
),
confirmText: "Deletar Permanentemente",
cancelText: "Cancelar",
requireTypedConfirmation: true,
confirmationWord: "DELETAR",
isDangerous: true,
onConfirm: async () => {
setLoading(true);
try {
await userService.deleteUser(userId);
toast.success(`Usuário "${userName}" deletado permanentemente!`);
loadUsuarios();
} catch (error: unknown) {
console.error("Erro ao deletar usuário:", error);
const errorMessage =
(error as { response?: { data?: { error?: string } } })?.response
?.data?.error || "Erro ao deletar usuário";
toast.error(errorMessage);
} finally {
setLoading(false);
}
},
});
};
// Funções de gerenciamento de roles
const handleAddRole = async () => {
if (!managingRolesUser) return;
try {
await userService.addUserRole(managingRolesUser.user.id, newRole);
toast.success(`Role "${newRole}" adicionada com sucesso!`);
setNewRole("user");
await loadUsuarios();
// Atualiza o estado local do modal
const updatedUsers = usuarios.find(
(u) => u.user.id === managingRolesUser.user.id
);
if (updatedUsers) {
setManagingRolesUser(updatedUsers);
}
} catch (error) {
console.error("Erro ao adicionar role:", error);
toast.error("Erro ao adicionar role");
}
};
const handleRemoveRole = async (role: UserRole) => {
if (!managingRolesUser) return;
if (!confirm(`Tem certeza que deseja remover a role "${role}"?`)) return;
try {
await userService.removeUserRole(managingRolesUser.user.id, role);
toast.success(`Role "${role}" removida com sucesso!`);
await loadUsuarios();
// Atualiza o estado local do modal
const updatedUsers = usuarios.find(
(u) => u.user.id === managingRolesUser.user.id
);
if (updatedUsers) {
setManagingRolesUser(updatedUsers);
}
} catch (error) {
console.error("Erro ao remover role:", error);
toast.error("Erro ao remover role");
}
};
// Funções de gerenciamento de pacientes
const handleEditPaciente = (paciente: Paciente) => {
setEditingPaciente(paciente);
setFormPaciente({
full_name: paciente.full_name,
cpf: paciente.cpf || "",
email: paciente.email || "",
phone_mobile: paciente.phone_mobile || "",
birth_date: paciente.birth_date || "",
social_name: paciente.social_name || "",
sex: paciente.sex || "",
blood_type: paciente.blood_type || "",
weight_kg: paciente.weight_kg?.toString() || "",
height_m: paciente.height_m?.toString() || "",
street: paciente.street || "",
number: paciente.number || "",
complement: paciente.complement || "",
neighborhood: paciente.neighborhood || "",
city: paciente.city || "",
state: paciente.state || "",
cep: paciente.cep || "",
});
setShowPacienteModal(true);
};
const handleSavePaciente = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const patientData = {
full_name: formPaciente.full_name,
cpf: formPaciente.cpf.replace(/\D/g, ""), // Remover máscara do CPF
email: formPaciente.email,
phone_mobile: formPaciente.phone_mobile,
birth_date: formPaciente.birth_date,
social_name: formPaciente.social_name,
sex: formPaciente.sex,
blood_type: formPaciente.blood_type,
weight_kg: formPaciente.weight_kg
? parseFloat(formPaciente.weight_kg)
: undefined,
height_m: formPaciente.height_m
? parseFloat(formPaciente.height_m)
: undefined,
street: formPaciente.street,
number: formPaciente.number,
complement: formPaciente.complement,
neighborhood: formPaciente.neighborhood,
city: formPaciente.city,
state: formPaciente.state,
cep: formPaciente.cep,
};
if (editingPaciente) {
await patientService.update(editingPaciente.id, patientData);
toast.success("Paciente atualizado com sucesso!");
setShowPacienteModal(false);
setEditingPaciente(null);
resetFormPaciente();
loadPacientes();
} else {
// Usar create-user com create_patient_record=true (nova API 21/10)
// isPublicRegistration = false porque é admin criando
await userService.createUser(
{
email: patientData.email,
full_name: patientData.full_name,
phone: patientData.phone_mobile,
role: "paciente",
create_patient_record: true,
cpf: patientData.cpf,
phone_mobile: patientData.phone_mobile,
redirect_url:
"https://mediconnectbrasil.netlify.app/paciente/agendamento",
},
false
);
toast.success(
"Paciente criado com sucesso! Magic link enviado para o email."
);
setShowPacienteModal(false);
resetFormPaciente();
loadPacientes();
}
} catch (error) {
console.error("Erro ao salvar paciente:", error);
toast.error("Erro ao salvar paciente");
} finally {
setLoading(false);
}
};
const handleDeletePaciente = async (id: string, nome: string) => {
if (
!confirm(
`Tem certeza que deseja deletar o paciente "${nome}"? Esta ação não pode ser desfeita.`
)
) {
return;
}
try {
console.log("[PainelAdmin] Deletando paciente:", { id, nome });
await patientService.delete(id);
console.log("[PainelAdmin] Paciente deletado com sucesso");
toast.success("Paciente deletado com sucesso!");
loadPacientes();
} catch (error) {
console.error("[PainelAdmin] Erro ao deletar paciente:", error);
toast.error("Erro ao deletar paciente");
}
};
// Funções de gerenciamento de médicos
const handleEditMedico = (medico: Doctor) => {
setEditingMedico(medico);
setFormMedico({
crm: medico.crm || "",
crm_uf: medico.crm_uf || "SP",
specialty: medico.specialty || "",
full_name: medico.full_name || "",
cpf: medico.cpf || "",
email: medico.email || "",
phone_mobile: medico.phone_mobile || "",
phone2: medico.phone2 || "",
cep: medico.cep || "",
street: medico.street || "",
number: medico.number || "",
complement: medico.complement || "",
neighborhood: medico.neighborhood || "",
city: medico.city || "",
state: medico.state || "",
birth_date: medico.birth_date || "",
rg: medico.rg || "",
active: medico.active ?? true,
});
setShowMedicoModal(true);
};
const handleSaveMedico = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Preparar dados com CPF sem máscara
const medicoData = {
...formMedico,
cpf: formMedico.cpf.replace(/\D/g, ""), // Remover máscara do CPF
};
if (editingMedico) {
await doctorService.update(editingMedico.id!, medicoData);
toast.success("Médico atualizado com sucesso!");
setShowMedicoModal(false);
setEditingMedico(null);
resetFormMedico();
loadMedicos();
} else {
// Usar create-user com role=medico (nova API 21/10 - create-doctor não cria auth user)
// isPublicRegistration = false porque é admin criando
await userService.createUser(
{
email: medicoData.email,
full_name: medicoData.full_name,
phone: medicoData.phone_mobile,
role: "medico",
redirect_url: "https://mediconnectbrasil.netlify.app/medico/painel",
},
false
);
// Depois criar registro na tabela doctors com createDoctor (sem password)
await userService.createDoctor({
crm: medicoData.crm,
crm_uf: medicoData.crm_uf,
cpf: medicoData.cpf,
full_name: medicoData.full_name,
email: medicoData.email,
specialty: medicoData.specialty,
phone_mobile: medicoData.phone_mobile,
});
toast.success(
"Médico criado com sucesso! Magic link enviado para o email."
);
setShowMedicoModal(false);
resetFormMedico();
loadMedicos();
}
} catch (error) {
console.error("Erro ao salvar médico:", error);
toast.error("Erro ao salvar médico");
} finally {
setLoading(false);
}
};
const handleDeleteMedico = async (id: string, nome: string) => {
if (
!confirm(
`Tem certeza que deseja deletar o médico "${nome}"? Esta ação não pode ser desfeita.`
)
) {
return;
}
try {
await doctorService.delete(id);
toast.success("Médico deletado com sucesso!");
loadMedicos();
} catch {
toast.error("Erro ao deletar médico");
}
};
const resetFormPaciente = () => {
setFormPaciente({
full_name: "",
cpf: "",
email: "",
phone_mobile: "",
birth_date: "",
social_name: "",
sex: "",
blood_type: "",
weight_kg: "",
height_m: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
cep: "",
});
};
const resetFormUser = () => {
setFormUser({
email: "",
full_name: "",
phone: "",
role: "user",
});
};
const resetFormMedico = () => {
setFormMedico({
crm: "",
crm_uf: "SP",
specialty: "",
full_name: "",
cpf: "",
email: "",
phone_mobile: "",
phone2: "",
cep: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
birth_date: "",
rg: "",
active: true,
});
};
const estadosBR = [
"AC",
"AL",
"AP",
"AM",
"BA",
"CE",
"DF",
"ES",
"GO",
"MA",
"MT",
"MS",
"MG",
"PA",
"PB",
"PR",
"PE",
"PI",
"RJ",
"RN",
"RS",
"RO",
"RR",
"SC",
"SP",
"SE",
"TO",
];
const availableRoles: UserRole[] = [
"admin",
"gestor",
"medico",
"secretaria",
"user",
"paciente",
];
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="bg-white rounded-xl shadow p-6 mb-6 border border-gray-200">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Painel de Administração
</h1>
<p className="text-gray-600">
Gerenciar pacientes, usuários e médicos do sistema
</p>
</div>
<UserCog className="w-12 h-12 text-blue-600" />
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-xl shadow border border-gray-200 mb-6">
<div
className="flex border-b"
role="tablist"
aria-label="Seções do administrador"
>
<button
onClick={() => setActiveTab("pacientes")}
id="tab-pacientes-tab"
role="tab"
aria-selected={activeTab === "pacientes"}
aria-controls="tab-pacientes-panel"
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
activeTab === "pacientes"
? "border-b-2 border-blue-600 text-blue-600"
: "text-gray-600 hover:text-blue-600"
}`}
>
<Users className="w-5 h-5" />
Pacientes
</button>
<button
onClick={() => setActiveTab("usuarios")}
id="tab-usuarios-tab"
role="tab"
aria-selected={activeTab === "usuarios"}
aria-controls="tab-usuarios-panel"
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
activeTab === "usuarios"
? "border-b-2 border-blue-600 text-blue-600"
: "text-gray-600 hover:text-blue-600"
}`}
>
<UserPlus className="w-5 h-5" />
Usuários
</button>
<button
onClick={() => setActiveTab("medicos")}
id="tab-medicos-tab"
role="tab"
aria-selected={activeTab === "medicos"}
aria-controls="tab-medicos-panel"
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
activeTab === "medicos"
? "border-b-2 border-blue-600 text-blue-600"
: "text-gray-600 hover:text-blue-600"
}`}
>
<Stethoscope className="w-5 h-5" />
Médicos
</button>
</div>
{/* Content */}
<div className="p-6">
{activeTab === "pacientes" && (
<div
id="tab-pacientes-panel"
role="tabpanel"
aria-labelledby="tab-pacientes-tab"
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Pacientes Cadastrados</h2>
<button
onClick={() => setShowPacienteModal(true)}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
>
<Plus className="w-5 h-5" />
Novo Paciente
</button>
</div>
{loading ? (
<div className="text-center py-8">Carregando...</div>
) : (
<div className="grid gap-4">
{pacientes.length === 0 ? (
<div className="text-center py-8 text-gray-500">
Nenhum paciente cadastrado
</div>
) : (
pacientes.map((p, idx) => (
<div
key={p.id}
className={`rounded-lg p-4 flex items-center justify-between transition-colors ${
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
} hover:bg-gray-100`}
>
<div>
<h3 className="font-semibold text-lg">
{p.full_name}
</h3>
<p className="text-gray-600 text-sm">
{p.email} | {p.phone_mobile}
</p>
<p className="text-gray-500 text-xs">
CPF: {p.cpf} | Nascimento: {p.birth_date}
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => handleEditPaciente(p)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
title="Editar"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() =>
p.id && handleDeletePaciente(p.id, p.full_name)
}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
title="Deletar"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))
)}
</div>
)}
</div>
)}
{activeTab === "usuarios" && (
<div
id="tab-usuarios-panel"
role="tabpanel"
aria-labelledby="tab-usuarios-tab"
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Gerenciar Usuários</h2>
<div className="flex gap-2">
<button
onClick={loadUsuarios}
disabled={loading}
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
>
<RefreshCw
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
/>
Atualizar
</button>
<button
onClick={() => setShowUserModal(true)}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
<Plus className="w-5 h-5" />
Novo Usuário
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<input
type="text"
placeholder="Buscar por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
/>
</div>
{loading ? (
<div className="text-center py-8">Carregando usuários...</div>
) : (
<div className="overflow-auto max-h-[70vh] bg-white rounded-lg border border-gray-200">
<table className="w-full">
<thead className="bg-gradient-to-r from-blue-600 to-purple-600 text-white sticky top-0 z-10">
<tr>
<th className="px-4 py-3 text-left text-sm font-semibold">
Nome
</th>
<th className="px-4 py-3 text-left text-sm font-semibold">
Email
</th>
<th className="px-4 py-3 text-left text-sm font-semibold">
Telefone
</th>
<th className="px-4 py-3 text-left text-sm font-semibold">
Roles
</th>
<th className="px-4 py-3 text-left text-sm font-semibold">
Status
</th>
<th className="px-4 py-3 text-center text-sm font-semibold">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{usuarios.filter((user) => {
const searchLower = searchTerm.toLowerCase();
return (
user.profile?.full_name
?.toLowerCase()
.includes(searchLower) ||
user.profile?.email
?.toLowerCase()
.includes(searchLower) ||
user.user.email.toLowerCase().includes(searchLower)
);
}).length === 0 ? (
<tr>
<td
colSpan={6}
className="px-4 py-8 text-center text-gray-500"
>
{searchTerm
? "Nenhum usuário encontrado"
: "Nenhum usuário cadastrado"}
</td>
</tr>
) : (
usuarios
.filter((user) => {
const searchLower = searchTerm.toLowerCase();
return (
user.profile?.full_name
?.toLowerCase()
.includes(searchLower) ||
user.profile?.email
?.toLowerCase()
.includes(searchLower) ||
user.user.email
.toLowerCase()
.includes(searchLower)
);
})
.map((user) => (
<tr
key={user.user.id}
className="hover:bg-gray-50 odd:bg-white even:bg-gray-50"
>
<td className="px-4 py-3">
<div className="font-medium text-gray-900">
{user.profile?.full_name || "Sem nome"}
</div>
</td>
<td className="px-4 py-3 text-gray-600">
{user.profile?.email || user.user.email}
</td>
<td className="px-4 py-3 text-gray-600">
{user.profile?.phone || "-"}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{user.roles && user.roles.length > 0 ? (
user.roles.map((role, index) => (
<span
key={index}
className={`px-2 py-1 rounded text-xs font-semibold ${
role === "admin"
? "bg-purple-100 text-purple-700"
: role === "gestor"
? "bg-blue-100 text-blue-700"
: role === "medico"
? "bg-indigo-100 text-indigo-700"
: role === "secretaria"
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-700"
}`}
>
{role}
</span>
))
) : (
<span className="text-gray-400 text-xs">
Sem roles
</span>
)}
</div>
</td>
<td className="px-4 py-3">
<span
className={`px-3 py-1 rounded-full text-xs font-semibold ${
user.profile?.disabled
? "bg-red-100 text-red-700"
: "bg-green-100 text-green-700"
}`}
>
{user.profile?.disabled
? "Desabilitado"
: "Ativo"}
</span>
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleEditUser(user)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
title="Editar"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={async () => {
setManagingRolesUser(user);
const result =
await adminUserService.getUserRoles(
user.user.id
);
if (result.success && result.data) {
setUserRoles(result.data);
}
}}
className="p-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
title="Gerenciar Roles"
>
<Shield className="w-4 h-4" />
</button>
<button
onClick={() =>
handleToggleStatusUser(
user.user.id,
!!user.profile?.disabled
)
}
className={`p-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
user.profile?.disabled
? "text-green-600 hover:bg-green-50 focus-visible:ring-green-500"
: "text-orange-600 hover:bg-orange-50 focus-visible:ring-orange-500"
}`}
title={
user.profile?.disabled
? "Habilitar"
: "Desabilitar"
}
>
{user.profile?.disabled ? (
<UserCheck className="w-4 h-4" />
) : (
<UserX className="w-4 h-4" />
)}
</button>
<button
onClick={() =>
handleDeleteUser(
user.user.id,
user.profile?.full_name ||
user.user.email
)
}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
title="Deletar"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
)}
{activeTab === "medicos" && (
<div
id="tab-medicos-panel"
role="tabpanel"
aria-labelledby="tab-medicos-tab"
>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">Médicos Cadastrados</h2>
<button
onClick={() => setShowMedicoModal(true)}
className="flex items-center gap-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
>
<Plus className="w-5 h-5" />
Novo Médico
</button>
</div>
{loading ? (
<div className="text-center py-8">Carregando...</div>
) : (
<div className="grid gap-4">
{medicos.length === 0 ? (
<div className="text-center py-8 text-gray-500">
Nenhum médico cadastrado
</div>
) : (
medicos.map((m, idx) => (
<div
key={m.id}
className={`rounded-lg p-4 flex items-center justify-between transition-colors ${
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
} hover:bg-gray-100`}
>
<div className="flex-1">
<h3 className="font-semibold text-lg">
{m.full_name}
</h3>
<p className="text-gray-600 text-sm">
{m.specialty} | CRM: {m.crm}/{m.crm_uf}
</p>
<p className="text-gray-500 text-xs">
{m.email} | {m.phone_mobile}
</p>
</div>
<div className="flex items-center gap-2">
<span
className={`px-3 py-1 rounded-full text-sm ${
m.active
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{m.active ? "Ativo" : "Inativo"}
</span>
<button
onClick={() => handleEditMedico(m)}
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
title="Editar"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() =>
handleDeleteMedico(
m.id!,
m.full_name || "Médico sem nome"
)
}
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
title="Deletar"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))
)}
</div>
)}
</div>
)}
</div>
</div>
</div>
{/* Modal Paciente */}
{showPacienteModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="paciente-modal-title"
>
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h2 id="paciente-modal-title" className="text-2xl font-bold mb-4">
Novo Paciente
</h2>
<form onSubmit={handleSavePaciente} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
Nome Completo *
</label>
<input
type="text"
required
value={formPaciente.full_name}
onChange={(e) =>
setFormPaciente({
...formPaciente,
full_name: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Nome Social
</label>
<input
type="text"
value={formPaciente.social_name}
onChange={(e) =>
setFormPaciente({
...formPaciente,
social_name: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
CPF *
</label>
<input
type="text"
required
value={formPaciente.cpf}
onChange={(e) =>
setFormPaciente({
...formPaciente,
cpf: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
placeholder="00000000000"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Email *
</label>
<input
type="email"
required
value={formPaciente.email}
onChange={(e) =>
setFormPaciente({
...formPaciente,
email: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Telefone *
</label>
<input
type="text"
required
value={formPaciente.phone_mobile}
onChange={(e) =>
setFormPaciente({
...formPaciente,
phone_mobile: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
placeholder="(00) 00000-0000"
/>
</div>
{!editingPaciente && (
<div className="col-span-2 bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-blue-800">
🔐 <strong>Ativação de Conta:</strong> Um link mágico
(magic link) será enviado automaticamente para o email
do paciente para ativar a conta e definir senha.
</p>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">
Data de Nascimento
</label>
<input
type="date"
value={formPaciente.birth_date}
onChange={(e) =>
setFormPaciente({
...formPaciente,
birth_date: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Sexo
</label>
<select
value={formPaciente.sex}
onChange={(e) =>
setFormPaciente({
...formPaciente,
sex: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
>
<option value="">Selecione</option>
<option value="M">Masculino</option>
<option value="F">Feminino</option>
<option value="Outro">Outro</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Tipo Sanguíneo
</label>
<input
type="text"
value={formPaciente.blood_type}
onChange={(e) =>
setFormPaciente({
...formPaciente,
blood_type: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
placeholder="A+"
/>
</div>
</div>
<div className="flex gap-2 justify-end pt-4">
<button
type="button"
onClick={() => {
setShowPacienteModal(false);
setEditingPaciente(null);
resetFormPaciente();
}}
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
>
Cancelar
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
>
{loading
? editingPaciente
? "Salvando..."
: "Criando..."
: editingPaciente
? "Salvar"
: "Criar Paciente"}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal Usuário */}
{showUserModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="usuario-modal-title"
>
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-md w-full">
<div className="p-6">
<h2 id="usuario-modal-title" className="text-2xl font-bold mb-4">
Novo Usuário
</h2>
<form onSubmit={handleCreateUser} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">
Nome Completo *
</label>
<input
type="text"
required
value={formUser.full_name}
onChange={(e) =>
setFormUser({ ...formUser, full_name: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Email *
</label>
<input
type="email"
required
value={formUser.email}
onChange={(e) =>
setFormUser({ ...formUser, email: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Telefone
</label>
<input
type="text"
value={formUser.phone || ""}
onChange={(e) =>
setFormUser({ ...formUser, phone: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
placeholder="(00) 00000-0000"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Role/Papel *
</label>
<select
required
value={formUser.role}
onChange={(e) =>
setFormUser({
...formUser,
role: e.target.value as UserRole,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
>
{availableRoles.map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
</div>
{/* Toggle para criar com senha */}
<div className="border-t pt-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={usePassword}
onChange={(e) => setUsePassword(e.target.checked)}
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium">
Criar com senha (alternativa ao Magic Link)
</span>
</label>
</div>
{/* Campo de senha (condicional) */}
{usePassword && (
<div>
<label className="block text-sm font-medium mb-1">
Senha *
</label>
<input
type="password"
required={usePassword}
value={userPassword}
onChange={(e) => setUserPassword(e.target.value)}
minLength={6}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
placeholder="Mínimo 6 caracteres"
/>
<p className="text-xs text-gray-500 mt-1">
O usuário precisará confirmar o email antes de fazer login
</p>
</div>
)}
{!usePassword && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<p className="text-xs text-blue-700">
Um Magic Link será enviado para o email do usuário para
ativação da conta
</p>
</div>
)}
<div className="flex gap-2 justify-end pt-4">
<button
type="button"
onClick={() => {
setShowUserModal(false);
resetFormUser();
}}
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
>
Cancelar
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
{loading ? "Criando..." : "Criar Usuário"}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal Médico */}
{showMedicoModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="medico-modal-title"
>
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h2 id="medico-modal-title" className="text-2xl font-bold mb-4">
Novo Médico
</h2>
<form onSubmit={handleSaveMedico} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
Nome Completo *
</label>
<input
type="text"
required
value={formMedico.full_name}
onChange={(e) =>
setFormMedico({
...formMedico,
full_name: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Especialidade
</label>
<input
type="text"
value={formMedico.specialty}
onChange={(e) =>
setFormMedico({
...formMedico,
specialty: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
placeholder="Cardiologia"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
CRM *
</label>
<input
type="text"
required
value={formMedico.crm}
onChange={(e) =>
setFormMedico({ ...formMedico, crm: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
placeholder="123456"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
UF do CRM *
</label>
<select
required
value={formMedico.crm_uf}
onChange={(e) =>
setFormMedico({ ...formMedico, crm_uf: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
>
{estadosBR.map((uf) => (
<option key={uf} value={uf}>
{uf}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">
CPF *
</label>
<input
type="text"
required
value={formMedico.cpf}
onChange={(e) =>
setFormMedico({ ...formMedico, cpf: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
placeholder="000.000.000-00"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">RG</label>
<input
type="text"
value={formMedico.rg}
onChange={(e) =>
setFormMedico({ ...formMedico, rg: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Email *
</label>
<input
type="email"
required
value={formMedico.email}
onChange={(e) =>
setFormMedico({ ...formMedico, email: e.target.value })
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Telefone
</label>
<input
type="text"
value={formMedico.phone_mobile}
onChange={(e) =>
setFormMedico({
...formMedico,
phone_mobile: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
placeholder="(00) 00000-0000"
/>
</div>
{!editingMedico && (
<div className="col-span-2 bg-blue-50 p-3 rounded-lg">
<p className="text-sm text-blue-800">
🔐 <strong>Ativação de Conta:</strong> Um link mágico
(magic link) será enviado automaticamente para o email
do médico para ativar a conta e definir senha.
</p>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">
Data de Nascimento
</label>
<input
type="date"
value={formMedico.birth_date}
onChange={(e) =>
setFormMedico({
...formMedico,
birth_date: e.target.value,
})
}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
/>
</div>
<div>
<label className="flex items-center gap-2 mt-6">
<input
type="checkbox"
checked={formMedico.active}
onChange={(e) =>
setFormMedico({
...formMedico,
active: e.target.checked,
})
}
className="w-4 h-4 focus:outline-none focus:ring-2 focus:ring-purple-600"
/>
<span className="text-sm font-medium">Ativo</span>
</label>
</div>
</div>
<div className="flex gap-2 justify-end pt-4">
<button
type="button"
onClick={() => {
setShowMedicoModal(false);
setEditingMedico(null);
resetFormMedico();
}}
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
>
Cancelar
</button>
<button
type="submit"
disabled={loading}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
>
{loading
? editingMedico
? "Salvando..."
: "Criando..."
: editingMedico
? "Salvar"
: "Criar Médico"}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal de Edição de Usuário */}
{editingUser && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="editar-usuario-title"
>
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
<h2
id="editar-usuario-title"
className="text-xl font-bold text-gray-900 mb-4"
>
Editar Usuário
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nome Completo
</label>
<input
type="text"
value={editForm.full_name || ""}
onChange={(e) =>
setEditForm({ ...editForm, full_name: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<input
type="email"
value={editForm.email || ""}
onChange={(e) =>
setEditForm({ ...editForm, email: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefone
</label>
<input
type="tel"
value={editForm.phone || ""}
onChange={(e) =>
setEditForm({ ...editForm, phone: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
/>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => setEditingUser(null)}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
>
Cancelar
</button>
<button
onClick={handleSaveEditUser}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
>
Salvar
</button>
</div>
</div>
</div>
)}
{/* Modal de Gerenciar Roles */}
{managingRolesUser && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
role="dialog"
aria-modal="true"
aria-labelledby="gerenciar-roles-title"
>
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
<h2
id="gerenciar-roles-title"
className="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2"
>
<Shield className="w-5 h-5 text-purple-600" />
Gerenciar Roles
</h2>
<p className="text-sm text-gray-600 mb-4">
{managingRolesUser.profile?.full_name ||
managingRolesUser.user.email}
</p>
{/* Lista de roles atuais */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Roles Atuais:
</h3>
<div className="flex flex-wrap gap-2">
{managingRolesUser &&
managingRolesUser.roles &&
managingRolesUser.roles.length > 0 ? (
managingRolesUser.roles.map((role, index) => (
<div
key={`${role}-${index}`}
className={`flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
role === "admin"
? "bg-purple-100 text-purple-700"
: role === "gestor"
? "bg-blue-100 text-blue-700"
: role === "medico"
? "bg-indigo-100 text-indigo-700"
: role === "secretaria"
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-700"
}`}
>
{role}
<button
onClick={() => handleRemoveRole(role)}
className="hover:bg-black hover:bg-opacity-10 rounded-full p-0.5"
title="Remover role"
>
<span className="sr-only">Remover role {role}</span>
</button>
</div>
))
) : (
<span className="text-gray-400 text-sm">
Nenhum role atribuído
</span>
)}
</div>
</div>
{/* Adicionar novo role */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-gray-700 mb-2">
Adicionar Role:
</h3>
<div className="flex gap-2">
<select
value={newRole}
onChange={(e) => setNewRole(e.target.value as UserRole)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="user">User</option>
<option value="paciente">Paciente</option>
<option value="secretaria">Secretaria</option>
<option value="medico">Médico</option>
<option value="gestor">Gestor</option>
<option value="admin">Admin</option>
</select>
<button
onClick={handleAddRole}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Adicionar
</button>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => {
setManagingRolesUser(null);
setNewRole("user");
}}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
>
Fechar
</button>
</div>
</div>
</div>
)}
{/* Dialog de Confirmação Customizado */}
<ConfirmDialog
isOpen={confirmDialog.isOpen}
onClose={() => setConfirmDialog((prev) => ({ ...prev, isOpen: false }))}
onConfirm={confirmDialog.onConfirm}
title={confirmDialog.title}
message={confirmDialog.message}
confirmText={confirmDialog.confirmText}
cancelText={confirmDialog.cancelText}
requireTypedConfirmation={confirmDialog.requireTypedConfirmation}
confirmationWord={confirmDialog.confirmationWord}
isDangerous={confirmDialog.isDangerous}
/>
</div>
);
};
export default PainelAdmin;