saveinmed/saveinmed-frontend/src/app/dashboard/components/CadastroSuperadminModal.tsx
NANDO9322 e280ffe6f5 feat: implementação completa do cadastro de superadmin
- Backend: Adiciona campos (Superadmin, CPF, NomeSocial) e migration
- Backend: Restringe criação de superadmin apenas para superadmins
- Frontend: Corrige modal de cadastro, endpoint da API e visibilidade de senha
- Frontend: Corrige erro de chave estrangeira (company_id)
2026-01-23 12:27:25 -03:00

393 lines
13 KiB
TypeScript

"use client";
import { useState } from "react";
import { X, AlertCircle, CheckCircle, Loader, Eye, EyeOff } from "lucide-react";
interface CadastroSuperadminModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
currentCompanyId?: string;
}
// Funções de máscara
const maskCPF = (value: string): string => {
return value
.replace(/\D/g, "")
.slice(0, 11)
.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4")
.replace(/(\d{3})(\d{3})(\d{3})$/, "$1.$2.$3")
.replace(/(\d{3})(\d{3})$/, "$1.$2");
};
const maskPhone = (value: string): string => {
return value
.replace(/\D/g, "")
.slice(0, 11)
.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3")
.replace(/(\d{2})(\d{4})$/, "($1) $2");
};
export default function CadastroSuperadminModal({
isOpen,
onClose,
onSuccess,
currentCompanyId,
}: CadastroSuperadminModalProps) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [formData, setFormData] = useState({
nome: "",
email: "",
telefone: "",
senha: "",
confirmaSenha: "",
cpf: "",
nomeSocial: "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
let formattedValue = value;
// Aplicar máscaras
if (name === "cpf") {
formattedValue = maskCPF(value);
} else if (name === "telefone") {
formattedValue = maskPhone(value);
}
setFormData((prev) => ({
...prev,
[name]: formattedValue,
}));
};
const validarFormulario = (): boolean => {
if (!formData.nome.trim()) {
setError("Nome é obrigatório");
return false;
}
if (!formData.email.trim()) {
setError("Email é obrigatório");
return false;
}
if (!formData.email.includes("@")) {
setError("Email inválido");
return false;
}
if (!formData.senha) {
setError("Senha é obrigatória");
return false;
}
if (formData.senha.length < 6) {
setError("Senha deve ter pelo menos 6 caracteres");
return false;
}
if (formData.senha !== formData.confirmaSenha) {
setError("As senhas não coincidem");
return false;
}
if (!formData.cpf.trim()) {
setError("CPF é obrigatório");
return false;
}
// Remover máscara para validar CPF
const cpfLimpo = formData.cpf.replace(/\D/g, "");
if (cpfLimpo.length !== 11) {
setError("CPF deve ter 11 dígitos");
return false;
}
return true;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!validarFormulario()) {
return;
}
setLoading(true);
try {
// Remover máscaras antes de enviar
const cpfLimpo = formData.cpf.replace(/\D/g, "");
const telefoneLimpo = formData.telefone.replace(/\D/g, "");
const payload = {
name: formData.nome,
username: formData.email,
email: formData.email,
password: formData.senha,
cpf: cpfLimpo,
superadmin: true,
role: "superadmin",
"nome-social": formData.nomeSocial || null,
company_id: currentCompanyId || "00000000-0000-0000-0000-000000000000", // Fallback if no company provided, though might fail if foreign key invalid
};
// Obter token do localStorage para enviar autorização
const token = localStorage.getItem('access_token');
const response = await fetch(
`${process.env.NEXT_PUBLIC_BFF_API_URL}/users`,
{
method: "POST",
headers: {
"accept": "application/json",
"Content-Type": "application/json",
...(token && { "Authorization": `Bearer ${token}` }),
},
body: JSON.stringify(payload),
}
);
if (response.ok) {
setSuccess(true);
setFormData({
nome: "",
email: "",
telefone: "",
senha: "",
confirmaSenha: "",
cpf: "",
nomeSocial: "",
});
// Fechar modal após 2 segundos
setTimeout(() => {
setSuccess(false);
onClose();
onSuccess?.();
}, 2000);
} else {
const errorData = await response.json().catch(() => ({}));
setError(
errorData.message ||
errorData.error ||
`Erro ao criar superadmin (${response.status})`
);
}
} catch (err) {
console.error("Erro ao criar superadmin:", err);
setError("Erro ao conectar com o servidor");
} finally {
setLoading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/30 backdrop-blur-md flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-2xl max-w-md w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">
Cadastrar Superadmin
</h2>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Conteúdo */}
<div className="p-6">
{success ? (
<div className="space-y-4 text-center py-8">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
Sucesso!
</h3>
<p className="text-gray-600 text-sm">
Superadmin cadastrado com sucesso
</p>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{/* Mensagem de Erro */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex gap-3">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
<p className="text-sm text-red-700">{error}</p>
</div>
)}
{/* Nome */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome *
</label>
<input
type="text"
name="nome"
value={formData.nome}
onChange={handleChange}
placeholder="Nome completo"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="email@example.com"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
</div>
{/* CPF */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
CPF *
</label>
<input
type="text"
name="cpf"
value={formData.cpf}
onChange={handleChange}
placeholder="000.000.000-00"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
</div>
{/* Telefone */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Telefone
</label>
<input
type="tel"
name="telefone"
value={formData.telefone}
onChange={handleChange}
placeholder="(11) 9999-9999"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
</div>
{/* Nome Social */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nome Social
</label>
<input
type="text"
name="nomeSocial"
value={formData.nomeSocial}
onChange={handleChange}
placeholder="Nome social (opcional)"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
/>
</div>
{/* Senha */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Senha *
</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
name="senha"
value={formData.senha}
onChange={handleChange}
placeholder="Digite a senha"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 pr-10"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none"
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Confirmar Senha */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Confirmar Senha *
</label>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
name="confirmaSenha"
value={formData.confirmaSenha}
onChange={handleChange}
placeholder="Confirme a senha"
disabled={loading}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 pr-10"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Botões */}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
disabled={loading}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors disabled:opacity-50"
>
Cancelar
</button>
<button
type="submit"
disabled={loading}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
>
{loading && <Loader className="w-4 h-4 animate-spin" />}
{loading ? "Cadastrando..." : "Cadastrar"}
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
}