- 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)
393 lines
13 KiB
TypeScript
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>
|
|
);
|
|
}
|