Merge pull request #36 from rede5/Front-back-integracao-task14
feat(team): fluxo completo de cadastro de equipe com senha, correções de vínculo e avatar
- Frontend: Adiciona campos de senha e visibilidade na tela de Equipe.
- Frontend: Implementa criação de usuário prévia ao cadastro do profissional.
- Backend (Auth): Remove criação duplicada de perfil e ativa usuários automaticamente.
- Backend (Auth): Inclui dados do profissional (avatar) na resposta do endpoint /me.
- Backend (Profissionais): Corrige chave de contexto ('role') para permitir vínculo correto de usuário.
- Backend (Profissionais): Sincroniza exclusão para remover conta de usuário ao deletar profissional.
- Docs: Atualização dos arquivos Swagger.
This commit is contained in:
commit
5d02775496
9 changed files with 259 additions and 28 deletions
|
|
@ -3030,6 +3030,10 @@ const docTemplate = `{
|
||||||
"tabela_free": {
|
"tabela_free": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"target_user_id": {
|
||||||
|
"description": "Optional: For admin creation",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"tem_estudio": {
|
"tem_estudio": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3024,6 +3024,10 @@
|
||||||
"tabela_free": {
|
"tabela_free": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"target_user_id": {
|
||||||
|
"description": "Optional: For admin creation",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"tem_estudio": {
|
"tem_estudio": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,9 @@ definitions:
|
||||||
type: integer
|
type: integer
|
||||||
tabela_free:
|
tabela_free:
|
||||||
type: string
|
type: string
|
||||||
|
target_user_id:
|
||||||
|
description: 'Optional: For admin creation'
|
||||||
|
type: string
|
||||||
tem_estudio:
|
tem_estudio:
|
||||||
type: boolean
|
type: boolean
|
||||||
tipo_cartao:
|
tipo_cartao:
|
||||||
|
|
|
||||||
|
|
@ -361,9 +361,20 @@ func (h *Handler) Me(c *gin.Context) {
|
||||||
CompanyID: empresaID,
|
CompanyID: empresaID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Note: We are not returning AccessToken/ExpiresAt here as they are already set/active.
|
|
||||||
// But to match loginResponse structure we can leave them empty or fill appropriately if we were refreshing.
|
if user.Role == "PHOTOGRAPHER" || user.Role == "BUSINESS_OWNER" {
|
||||||
// For session restore, we mainly need the User object.
|
profData, err := h.service.GetProfessionalByUserID(c.Request.Context(), uuid.UUID(user.ID.Bytes).String())
|
||||||
|
if err == nil && profData != nil {
|
||||||
|
resp.Profissional = map[string]interface{}{
|
||||||
|
"id": uuid.UUID(profData.ID.Bytes).String(),
|
||||||
|
"nome": profData.Nome,
|
||||||
|
"funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(),
|
||||||
|
"funcao_profissional": profData.FuncaoNome.String,
|
||||||
|
"equipamentos": profData.Equipamentos.String,
|
||||||
|
"avatar_url": profData.AvatarUrl.String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
c.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +479,7 @@ func (h *Handler) AdminCreateUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just reuse the request struct but call AdminCreateUser service
|
// Just reuse the request struct but call AdminCreateUser service
|
||||||
user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome)
|
user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
|
c.JSON(http.StatusConflict, gin.H{"error": "email already registered"})
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,7 @@ func (s *Service) ApproveUser(ctx context.Context, id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome string) (*generated.Usuario, error) {
|
func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome string, ativo bool) (*generated.Usuario, error) {
|
||||||
// Hash password
|
// Hash password
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -211,18 +211,22 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If needed, create partial professional profile or just ignore for admin creation
|
if ativo {
|
||||||
// For simplicity, if name is provided and it's a professional role, we can stub it.
|
// Approve user immediately
|
||||||
// But let's stick to basic user creation first as per plan.
|
err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String())
|
||||||
// If the Admin creates a user, they might need to go to another endpoint to set profile.
|
if err != nil {
|
||||||
// Or we can optionally create if name is present.
|
// Log error but don't fail user creation? Or fail?
|
||||||
if (role == RolePhotographer || role == RoleBusinessOwner) && nome != "" {
|
// Better to return error
|
||||||
userID := uuid.UUID(user.ID.Bytes).String()
|
return nil, err
|
||||||
_, _ = s.profissionaisService.Create(ctx, userID, profissionais.CreateProfissionalInput{
|
}
|
||||||
Nome: nome,
|
// Refresh user object to reflect changes if needed, but ID and Email are same.
|
||||||
})
|
user.Ativo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stub creation removed to prevent duplicate profiles.
|
||||||
|
// The frontend is responsible for creating the full professional profile
|
||||||
|
// immediately after user creation via the professionals service.
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,7 +274,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error {
|
||||||
existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email)
|
existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// User not found (or error), try to create
|
// User not found (or error), try to create
|
||||||
user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name)
|
user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -313,6 +317,22 @@ func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuario
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetProfessionalByUserID(ctx context.Context, userID string) (*generated.GetProfissionalByUsuarioIDRow, error) {
|
||||||
|
parsedUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var pgID pgtype.UUID
|
||||||
|
pgID.Bytes = parsedUUID
|
||||||
|
pgID.Valid = true
|
||||||
|
|
||||||
|
p, err := s.queries.GetProfissionalByUsuarioID(ctx, pgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
func toPgText(s *string) pgtype.Text {
|
func toPgText(s *string) pgtype.Text {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return pgtype.Text{Valid: false}
|
return pgtype.Text{Valid: false}
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,21 @@ func (h *Handler) Create(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Security: Only allow TargetUserID if user is ADMIN or OWNER
|
||||||
|
if input.TargetUserID != nil && *input.TargetUserID != "" {
|
||||||
|
userRole, exists := c.Get("role")
|
||||||
|
if !exists {
|
||||||
|
// Should validation fail? Or just ignore target?
|
||||||
|
// Safer to ignore target user ID if role not found
|
||||||
|
input.TargetUserID = nil
|
||||||
|
} else {
|
||||||
|
roleStr, ok := userRole.(string)
|
||||||
|
if !ok || (roleStr != "SUPERADMIN" && roleStr != "BUSINESS_OWNER") {
|
||||||
|
input.TargetUserID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prof, err := h.service.Create(c.Request.Context(), userIDStr, input)
|
prof, err := h.service.Create(c.Request.Context(), userIDStr, input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
|
|
||||||
|
|
@ -45,12 +45,18 @@ type CreateProfissionalInput struct {
|
||||||
Equipamentos *string `json:"equipamentos"`
|
Equipamentos *string `json:"equipamentos"`
|
||||||
Email *string `json:"email"`
|
Email *string `json:"email"`
|
||||||
AvatarURL *string `json:"avatar_url"`
|
AvatarURL *string `json:"avatar_url"`
|
||||||
|
TargetUserID *string `json:"target_user_id"` // Optional: For admin creation
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.CadastroProfissionai, error) {
|
func (s *Service) Create(ctx context.Context, userID string, input CreateProfissionalInput) (*generated.CadastroProfissionai, error) {
|
||||||
usuarioUUID, err := uuid.Parse(userID)
|
finalUserID := userID
|
||||||
|
if input.TargetUserID != nil && *input.TargetUserID != "" {
|
||||||
|
finalUserID = *input.TargetUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
usuarioUUID, err := uuid.Parse(finalUserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid usuario_id from context")
|
return nil, errors.New("invalid usuario_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
var funcaoUUID uuid.UUID
|
var funcaoUUID uuid.UUID
|
||||||
|
|
@ -198,7 +204,35 @@ func (s *Service) Delete(ctx context.Context, id string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("invalid id")
|
return errors.New("invalid id")
|
||||||
}
|
}
|
||||||
return s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
|
|
||||||
|
// Get professional to find associated user
|
||||||
|
prof, err := s.queries.GetProfissionalByID(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete professional profile (should be done first or after?)
|
||||||
|
// If foreign key is SET NULL, it doesn't strictly matter for FK constraint,
|
||||||
|
// but logically deleting the profile first is cleaner if we want to ensure profile is gone.
|
||||||
|
// Actually, if we delete User first and it SETS NULL, we still have to delete Profile.
|
||||||
|
// So let's delete Profile first.
|
||||||
|
err = s.queries.DeleteProfissional(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete associated user if exists
|
||||||
|
if prof.UsuarioID.Valid {
|
||||||
|
err = s.queries.DeleteUsuario(ctx, prof.UsuarioID)
|
||||||
|
if err != nil {
|
||||||
|
// Create warning log? For now just return error or ignore?
|
||||||
|
// If user deletion fails, it's orphan but harmless-ish (except login).
|
||||||
|
// Better to return error.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Check,
|
Check,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
import {
|
import {
|
||||||
|
|
@ -64,10 +66,12 @@ export const TeamPage: React.FC = () => {
|
||||||
const [viewProfessional, setViewProfessional] = useState<Professional | null>(null);
|
const [viewProfessional, setViewProfessional] = useState<Professional | null>(null);
|
||||||
|
|
||||||
// Form State
|
// Form State
|
||||||
const initialFormState: CreateProfessionalDTO = {
|
const initialFormState: CreateProfessionalDTO & { senha?: string; confirmarSenha?: string } = {
|
||||||
nome: "",
|
nome: "",
|
||||||
funcao_profissional_id: "",
|
funcao_profissional_id: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
senha: "",
|
||||||
|
confirmarSenha: "",
|
||||||
whatsapp: "",
|
whatsapp: "",
|
||||||
cpf_cnpj_titular: "",
|
cpf_cnpj_titular: "",
|
||||||
endereco: "",
|
endereco: "",
|
||||||
|
|
@ -92,9 +96,12 @@ export const TeamPage: React.FC = () => {
|
||||||
avatar_url: "",
|
avatar_url: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formData, setFormData] = useState<CreateProfessionalDTO>(initialFormState);
|
const [formData, setFormData] = useState<CreateProfessionalDTO & { senha?: string; confirmarSenha?: string }>(initialFormState);
|
||||||
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
||||||
const [avatarPreview, setAvatarPreview] = useState<string>("");
|
const [avatarPreview, setAvatarPreview] = useState<string>("");
|
||||||
|
// Password Visibility
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -228,6 +235,8 @@ export const TeamPage: React.FC = () => {
|
||||||
nome: professional.nome,
|
nome: professional.nome,
|
||||||
funcao_profissional_id: professional.funcao_profissional_id,
|
funcao_profissional_id: professional.funcao_profissional_id,
|
||||||
email: professional.email || "",
|
email: professional.email || "",
|
||||||
|
senha: "", // Não editamos senha aqui
|
||||||
|
confirmarSenha: "",
|
||||||
whatsapp: professional.whatsapp || "",
|
whatsapp: professional.whatsapp || "",
|
||||||
cpf_cnpj_titular: professional.cpf_cnpj_titular || "",
|
cpf_cnpj_titular: professional.cpf_cnpj_titular || "",
|
||||||
endereco: professional.endereco || "",
|
endereco: professional.endereco || "",
|
||||||
|
|
@ -287,6 +296,20 @@ export const TeamPage: React.FC = () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Validation for password on creation
|
||||||
|
if (!isEdit && (formData.senha || formData.confirmarSenha)) {
|
||||||
|
if (formData.senha !== formData.confirmarSenha) {
|
||||||
|
alert("As senhas não coincidem!");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (formData.senha && formData.senha.length < 6) {
|
||||||
|
alert("A senha deve ter pelo menos 6 caracteres.");
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let finalAvatarUrl = formData.avatar_url;
|
let finalAvatarUrl = formData.avatar_url;
|
||||||
|
|
||||||
// Handle Avatar Upload if new file selected
|
// Handle Avatar Upload if new file selected
|
||||||
|
|
@ -298,13 +321,52 @@ export const TeamPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = { ...formData, avatar_url: finalAvatarUrl };
|
const payload: any = { ...formData, avatar_url: finalAvatarUrl };
|
||||||
|
// Remove password fields from professional payload
|
||||||
|
delete payload.senha;
|
||||||
|
delete payload.confirmarSenha;
|
||||||
|
|
||||||
if (isEdit && selectedProfessional) {
|
if (isEdit && selectedProfessional) {
|
||||||
await updateProfessional(selectedProfessional.id, payload, token);
|
await updateProfessional(selectedProfessional.id, payload, token);
|
||||||
alert("Profissional atualizado com sucesso!");
|
alert("Profissional atualizado com sucesso!");
|
||||||
} else {
|
} else {
|
||||||
await createProfessional(payload, token);
|
// Create User First (if password provided or mandatory logic?)
|
||||||
|
// If password is provided, we must create a user account.
|
||||||
|
// User requested: "ao cadastrar um novo profissional falta cadastrar a senha ... pra que esse profissional acesse a interface"
|
||||||
|
// So we should try to create user first.
|
||||||
|
let targetUserId = "";
|
||||||
|
|
||||||
|
if (formData.email && formData.senha) {
|
||||||
|
const { adminCreateUser } = await import("../services/apiService");
|
||||||
|
const createRes = await adminCreateUser({
|
||||||
|
email: formData.email,
|
||||||
|
senha: formData.senha,
|
||||||
|
nome: formData.nome,
|
||||||
|
role: "PHOTOGRAPHER", // Default role for professionals created here? Or map from selected role?
|
||||||
|
// Mapear função? Usually PHOTOGRAPHER or generic. Let's assume PHOTOGRAPHER for now as they are "Equipe".
|
||||||
|
tipo_profissional: roles.find(r => r.id === formData.funcao_profissional_id)?.nome || "",
|
||||||
|
ativo: true, // Auto-active as per request
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
if (createRes.error) {
|
||||||
|
// If user API fails (e.g. email exists), we stop? Or let create professional proceed unlinked?
|
||||||
|
// User requirement implies linked account.
|
||||||
|
// If email exists, maybe we can't create user, but we can check if we should link to existing?
|
||||||
|
// For simplicity, error out.
|
||||||
|
throw new Error("Erro ao criar usuário de login: " + createRes.error);
|
||||||
|
}
|
||||||
|
if (createRes.data && createRes.data.id) {
|
||||||
|
targetUserId = createRes.data.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUserId) {
|
||||||
|
payload.target_user_id = targetUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await createProfessional(payload, token);
|
||||||
|
if (res.error) throw new Error(res.error);
|
||||||
|
|
||||||
alert("Profissional criado com sucesso!");
|
alert("Profissional criado com sucesso!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,11 +374,10 @@ export const TeamPage: React.FC = () => {
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
fetchData();
|
fetchData();
|
||||||
// Reset form
|
// Reset form
|
||||||
// Reset form
|
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error submitting form:", error);
|
console.error("Error submitting form:", error);
|
||||||
alert("Erro ao salvar profissional. Verifique o console.");
|
alert(error.message || "Erro ao salvar profissional. Verifique o console.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|
@ -553,9 +614,53 @@ export const TeamPage: React.FC = () => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Email</label>
|
<label className="block text-sm font-medium text-gray-700">Email *</label>
|
||||||
<input type="email" value={formData.email} onChange={e => setFormData({ ...formData, email: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
|
<input required type="email" value={formData.email} onChange={e => setFormData({ ...formData, email: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
|
||||||
</div>
|
</div>
|
||||||
|
{!showEditModal && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Senha *</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
value={formData.senha}
|
||||||
|
onChange={e => setFormData({ ...formData, senha: e.target.value })}
|
||||||
|
minLength={6}
|
||||||
|
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Confirmar Senha *</label>
|
||||||
|
<div className="relative mt-1">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
type={showConfirmPassword ? "text" : "password"}
|
||||||
|
value={formData.confirmarSenha}
|
||||||
|
onChange={e => setFormData({ ...formData, confirmarSenha: e.target.value })}
|
||||||
|
minLength={6}
|
||||||
|
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border pr-10"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600"
|
||||||
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
|
>
|
||||||
|
{showConfirmPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">WhatsApp</label>
|
<label className="block text-sm font-medium text-gray-700">WhatsApp</label>
|
||||||
<input type="text" value={formData.whatsapp} onChange={e => setFormData({ ...formData, whatsapp: maskPhone(e.target.value) })} maxLength={15} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
|
<input type="text" value={formData.whatsapp} onChange={e => setFormData({ ...formData, whatsapp: maskPhone(e.target.value) })} maxLength={15} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
|
||||||
|
|
|
||||||
|
|
@ -753,6 +753,41 @@ export async function updateEventStatus(token: string, eventId: string, status:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um usuário pela interface administrativa
|
||||||
|
*/
|
||||||
|
export async function adminCreateUser(data: any, token: string): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/admin/users`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
return {
|
||||||
|
data: responseData,
|
||||||
|
error: null,
|
||||||
|
isBackendDown: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating admin user:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtém URL pré-assinada para upload de arquivo
|
* Obtém URL pré-assinada para upload de arquivo
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue