riseup-squad18/src/pages/GerenciarUsuarios.tsx
guisilvagomes 3443e46ca3 feat: implementa chatbot AI, gerenciamento de disponibilidade médica, visualização de laudos e melhorias no painel da secretária
- Adiciona chatbot AI com interface responsiva e posicionamento otimizado
- Implementa gerenciamento completo de disponibilidade e exceções médicas
- Adiciona modal de visualização detalhada de laudos no painel do paciente
- Corrige relatórios da secretária para mostrar nomes de médicos
- Implementa mensagem de boas-vindas personalizada com nome real
- Remove mensagens duplicadas de login
- Remove arquivo cleanup-deps.ps1 desnecessário
- Atualiza README com todas as novas funcionalidades
2025-11-05 16:51:33 -03:00

912 lines
34 KiB
TypeScript

import React, { useState, useEffect } from "react";
import {
Users,
Edit,
Trash2,
UserCheck,
UserX,
Search,
RefreshCw,
Shield,
Plus,
X,
} from "lucide-react";
import toast from "react-hot-toast";
import adminUserService, {
FullUserInfo,
UpdateUserData,
UserRole,
} from "../services/adminUserService";
const GerenciarUsuarios: React.FC = () => {
const [usuarios, setUsuarios] = useState<FullUserInfo[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [editingUser, setEditingUser] = useState<FullUserInfo | null>(null);
const [editForm, setEditForm] = useState<UpdateUserData>({});
const [managingRolesUser, setManagingRolesUser] =
useState<FullUserInfo | null>(null);
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
const [newRole, setNewRole] = useState<string>("");
const [showCreateModal, setShowCreateModal] = useState(false);
const [createForm, setCreateForm] = useState({
email: "",
password: "",
full_name: "",
phone_mobile: "",
cpf: "",
role: "",
create_patient_record: false,
usePassword: true,
});
useEffect(() => {
carregarUsuarios();
}, []);
const carregarUsuarios = async () => {
setLoading(true);
try {
const result = await adminUserService.listAllUsers();
if (result.success && result.data) {
setUsuarios(result.data);
} else {
toast.error(result.error || "Erro ao carregar usuários");
}
} catch {
toast.error("Erro ao carregar usuários");
} finally {
setLoading(false);
}
};
const handleEdit = (user: FullUserInfo) => {
setEditingUser(user);
setEditForm({
full_name: user.profile?.full_name || "",
email: user.profile?.email || "",
phone: user.profile?.phone || "",
disabled: user.profile?.disabled || false,
});
};
const handleSaveEdit = async () => {
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);
carregarUsuarios();
} else {
toast.error(result.error || "Erro ao atualizar usuário");
}
} catch {
toast.error("Erro ao atualizar usuário");
}
};
const handleToggleStatus = async (userId: string, currentStatus: boolean) => {
try {
const result = currentStatus
? await adminUserService.enableUser(userId)
: await adminUserService.disableUser(userId);
if (result.success) {
toast.success(
`Usuário ${
currentStatus ? "habilitado" : "desabilitado"
} com sucesso!`
);
carregarUsuarios();
} else {
toast.error(result.error || "Erro ao alterar status do usuário");
}
} catch {
toast.error("Erro ao alterar status do usuário");
}
};
const handleDelete = async (userId: string, userName: string) => {
if (
!confirm(
`Tem certeza que deseja deletar o usuário "${userName}"? Esta ação não pode ser desfeita.`
)
) {
return;
}
try {
const result = await adminUserService.deleteUser(userId);
if (result.success) {
toast.success("Usuário deletado com sucesso!");
carregarUsuarios();
} else {
toast.error(result.error || "Erro ao deletar usuário");
}
} catch {
toast.error("Erro ao deletar usuário");
}
};
const handleCreateUser = async () => {
if (!createForm.email || !createForm.full_name || !createForm.role) {
toast.error("Preencha os campos obrigatórios");
return;
}
if (createForm.usePassword && !createForm.password) {
toast.error("Informe a senha");
return;
}
if (
createForm.create_patient_record &&
(!createForm.cpf || !createForm.phone_mobile)
) {
toast.error(
"CPF e telefone são obrigatórios para criar registro de paciente"
);
return;
}
try {
const endpoint = createForm.usePassword
? "/functions/v1/create-user-with-password"
: "/functions/v1/create-user";
const payload: any = {
email: createForm.email,
full_name: createForm.full_name,
role: createForm.role,
};
if (createForm.usePassword) {
payload.password = createForm.password;
}
if (createForm.phone_mobile) {
payload.phone_mobile = createForm.phone_mobile;
}
if (createForm.create_patient_record) {
payload.create_patient_record = true;
payload.cpf = createForm.cpf;
}
const response = await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co${endpoint}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apikey:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
Authorization: `Bearer ${localStorage.getItem(
"mediconnect_access_token"
)}`,
},
body: JSON.stringify(payload),
}
);
const data = await response.json();
if (response.ok) {
toast.success("Usuário criado com sucesso!");
setShowCreateModal(false);
setCreateForm({
email: "",
password: "",
full_name: "",
phone_mobile: "",
cpf: "",
role: "",
create_patient_record: false,
usePassword: true,
});
carregarUsuarios();
} else {
toast.error(data.message || data.error || "Erro ao criar usuário");
}
} catch (error) {
console.error("Erro ao criar usuário:", error);
toast.error("Erro ao criar usuário");
}
};
const usuariosFiltrados = 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)
);
});
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-white p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="bg-gradient-to-r from-indigo-600 to-purple-600 w-12 h-12 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">
Gerenciar Usuários
</h1>
<p className="text-gray-600">
Visualize e edite informações dos usuários
</p>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
>
<Plus className="w-4 h-4" />
Criar Usuário
</button>
<button
onClick={carregarUsuarios}
disabled={loading}
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
>
<RefreshCw
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
/>
Atualizar
</button>
</div>
</div>
{/* Search Bar */}
<div className="relative">
<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-indigo-600 focus:border-indigo-600/40"
/>
</div>
</div>
{/* Users Table */}
{loading ? (
<div className="bg-white rounded-lg shadow-lg p-12 text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Carregando usuários...</p>
</div>
) : (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="overflow-auto max-h-[70vh]">
<table className="w-full">
<thead className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white sticky top-0 z-10">
<tr>
<th className="px-6 py-3 text-left text-sm font-semibold">
Nome
</th>
<th className="px-6 py-3 text-left text-sm font-semibold">
Email
</th>
<th className="px-6 py-3 text-left text-sm font-semibold">
Telefone
</th>
<th className="px-6 py-3 text-left text-sm font-semibold">
Roles
</th>
<th className="px-6 py-3 text-left text-sm font-semibold">
Status
</th>
<th className="px-6 py-3 text-left text-sm font-semibold">
Criado em
</th>
<th className="px-6 py-3 text-center text-sm font-semibold">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{usuariosFiltrados.length === 0 ? (
<tr>
<td
colSpan={7}
className="px-6 py-8 text-center text-gray-500"
>
{searchTerm
? "Nenhum usuário encontrado"
: "Nenhum usuário cadastrado"}
</td>
</tr>
) : (
usuariosFiltrados.map((user, idx) => (
<tr
key={user.user.id}
className={`hover:bg-gray-50 ${
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
}`}
>
<td className="px-6 py-4">
<div className="font-medium text-gray-900">
{user.profile?.full_name || "Sem nome"}
</div>
</td>
<td className="px-6 py-4 text-gray-600">
{user.profile?.email || user.user.email}
</td>
<td className="px-6 py-4 text-gray-600">
{user.profile?.phone || "-"}
</td>
<td className="px-6 py-4">
<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-6 py-4">
<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-6 py-4 text-gray-600 text-sm">
{new Date(user.user.created_at).toLocaleDateString(
"pt-BR"
)}
</td>
<td className="px-6 py-4">
<div className="flex items-center justify-center gap-2">
<button
onClick={() => handleEdit(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={() =>
handleToggleStatus(
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={() =>
handleDelete(
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>
)}
</div>
{/* Modal de Edição */}
{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="form-input"
/>
</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="form-input"
/>
</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="form-input"
/>
</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={handleSaveEdit}
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-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">
{userRoles.length > 0 ? (
userRoles.map((userRole) => (
<div
key={userRole.id}
className={`flex items-center gap-1 px-3 py-1 rounded-full text-xs font-semibold ${
userRole.role === "admin"
? "bg-purple-100 text-purple-700"
: userRole.role === "gestor"
? "bg-blue-100 text-blue-700"
: userRole.role === "medico"
? "bg-indigo-100 text-indigo-700"
: userRole.role === "secretaria"
? "bg-green-100 text-green-700"
: "bg-gray-100 text-gray-700"
}`}
>
{userRole.role}
<button
onClick={async () => {
const result = await adminUserService.removeUserRole(
userRole.id
);
if (result.success) {
toast.success("Role removido com sucesso!");
const rolesResult =
await adminUserService.getUserRoles(
managingRolesUser.user.id
);
if (rolesResult.success && rolesResult.data) {
setUserRoles(rolesResult.data);
}
carregarUsuarios();
} else {
toast.error(result.error || "Erro ao remover role");
}
}}
className="hover:bg-black hover:bg-opacity-10 rounded-full p-0.5"
>
<X className="w-3 h-3" />
</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)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40 text-sm"
>
<option value="">Selecione um role...</option>
<option value="admin">Admin</option>
<option value="gestor">Gestor</option>
<option value="medico">Médico</option>
<option value="secretaria">Secretária</option>
<option value="user">Usuário</option>
</select>
<button
onClick={async () => {
if (!newRole) {
toast.error("Selecione um role");
return;
}
const result = await adminUserService.addUserRole(
managingRolesUser.user.id,
newRole as
| "admin"
| "gestor"
| "medico"
| "secretaria"
| "user"
);
if (result.success) {
toast.success("Role adicionado com sucesso!");
setNewRole("");
const rolesResult = await adminUserService.getUserRoles(
managingRolesUser.user.id
);
if (rolesResult.success && rolesResult.data) {
setUserRoles(rolesResult.data);
}
carregarUsuarios();
} else {
toast.error(result.error || "Erro ao adicionar role");
}
}}
disabled={!newRole}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
>
<Plus className="w-4 h-4" />
Adicionar
</button>
</div>
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => {
setManagingRolesUser(null);
setUserRoles([]);
setNewRole("");
}}
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>
)}
{/* Modal Criar Usuário */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-gray-900">
Criar Novo Usuário
</h2>
<button
onClick={() => setShowCreateModal(false)}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="space-y-4">
{/* Método de Autenticação */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Método de Autenticação
</label>
<div className="flex gap-4">
<label className="flex items-center">
<input
type="radio"
checked={createForm.usePassword}
onChange={() =>
setCreateForm({ ...createForm, usePassword: true })
}
className="mr-2"
/>
Email e Senha
</label>
<label className="flex items-center">
<input
type="radio"
checked={!createForm.usePassword}
onChange={() =>
setCreateForm({ ...createForm, usePassword: false })
}
className="mr-2"
/>
Magic Link (sem senha)
</label>
</div>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
value={createForm.email}
onChange={(e) =>
setCreateForm({ ...createForm, email: e.target.value })
}
className="form-input"
placeholder="usuario@exemplo.com"
/>
</div>
{/* Senha (somente se usePassword) */}
{createForm.usePassword && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Senha *
</label>
<input
type="password"
value={createForm.password}
onChange={(e) =>
setCreateForm({
...createForm,
password: e.target.value,
})
}
className="form-input"
placeholder="Mínimo 6 caracteres"
/>
</div>
)}
{/* Nome Completo */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nome Completo *
</label>
<input
type="text"
value={createForm.full_name}
onChange={(e) =>
setCreateForm({
...createForm,
full_name: e.target.value,
})
}
className="form-input"
placeholder="João da Silva"
/>
</div>
{/* Role */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Role *
</label>
<select
value={createForm.role}
onChange={(e) =>
setCreateForm({ ...createForm, role: e.target.value })
}
className="form-input"
>
<option value="">Selecione...</option>
<option value="admin">Admin</option>
<option value="gestor">Gestor</option>
<option value="medico">Médico</option>
<option value="secretaria">Secretária</option>
<option value="paciente">Paciente</option>
</select>
</div>
{/* Telefone */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Telefone
</label>
<input
type="text"
value={createForm.phone_mobile}
onChange={(e) =>
setCreateForm({
...createForm,
phone_mobile: e.target.value,
})
}
className="form-input"
placeholder="(11) 99999-9999"
/>
</div>
{/* Criar Registro de Paciente */}
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={createForm.create_patient_record}
onChange={(e) =>
setCreateForm({
...createForm,
create_patient_record: e.target.checked,
})
}
className="mr-2"
/>
<span className="text-sm font-medium text-gray-700">
Criar registro na tabela de pacientes
</span>
</label>
</div>
{/* CPF (obrigatório se create_patient_record) */}
{createForm.create_patient_record && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CPF *
</label>
<input
type="text"
value={createForm.cpf}
onChange={(e) =>
setCreateForm({
...createForm,
cpf: e.target.value.replace(/\D/g, ""),
})
}
className="form-input"
placeholder="12345678901"
maxLength={11}
/>
<p className="text-xs text-gray-500 mt-1">
Apenas números, 11 dígitos
</p>
</div>
)}
</div>
<div className="flex gap-3 mt-6">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
>
Cancelar
</button>
<button
onClick={handleCreateUser}
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Criar Usuário
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default GerenciarUsuarios;