fix: correções na criação de usuários admin e no formulário de aprovação
- Frontend: Refatoração do componente UserApproval para corrigir perda de foco nos inputs (extração de modais). - Backend: Implementação da criação automática do perfil profissional (cadastro_profissionais) ao criar um novo usuário admin. - Backend: Correção para evitar duplicidade de profissionais, utilizando o email para vincular ao perfil existente. - API: Ajuste para retornar dados completos (nome, telefone, empresa) na listagem de usuários do admin.
This commit is contained in:
parent
58ee90df73
commit
90e1508409
7 changed files with 594 additions and 200 deletions
|
|
@ -482,7 +482,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, true)
|
user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.TipoProfissional, 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"})
|
||||||
|
|
@ -585,12 +585,22 @@ func (h *Handler) ListUsers(c *gin.Context) {
|
||||||
|
|
||||||
resp := make([]map[string]interface{}, len(users))
|
resp := make([]map[string]interface{}, len(users))
|
||||||
for i, u := range users {
|
for i, u := range users {
|
||||||
|
empresaId := ""
|
||||||
|
if u.EmpresaID.Valid {
|
||||||
|
empresaId = uuid.UUID(u.EmpresaID.Bytes).String()
|
||||||
|
}
|
||||||
|
|
||||||
resp[i] = map[string]interface{}{
|
resp[i] = map[string]interface{}{
|
||||||
"id": uuid.UUID(u.ID.Bytes).String(),
|
"id": uuid.UUID(u.ID.Bytes).String(),
|
||||||
"email": u.Email,
|
"email": u.Email,
|
||||||
"role": u.Role,
|
"role": u.Role,
|
||||||
"ativo": u.Ativo,
|
"ativo": u.Ativo,
|
||||||
"created_at": u.CriadoEm.Time,
|
"created_at": u.CriadoEm.Time,
|
||||||
|
"name": u.Nome,
|
||||||
|
"phone": u.Whatsapp,
|
||||||
|
"company_name": u.EmpresaNome.String,
|
||||||
|
"company_id": empresaId,
|
||||||
|
"professional_type": u.TipoProfissional.String,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,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, ativo bool) (*generated.Usuario, error) {
|
func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, tipoProfissional 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 {
|
||||||
|
|
@ -205,9 +205,10 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{
|
||||||
Email: email,
|
Email: email,
|
||||||
SenhaHash: string(hashedPassword),
|
SenhaHash: string(hashedPassword),
|
||||||
Role: role,
|
Role: role,
|
||||||
|
TipoProfissional: toPgText(&tipoProfissional),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -217,17 +218,56 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome
|
||||||
// Approve user immediately
|
// Approve user immediately
|
||||||
err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String())
|
err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log error but don't fail user creation? Or fail?
|
|
||||||
// Better to return error
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Refresh user object to reflect changes if needed, but ID and Email are same.
|
// Refresh user object to reflect changes if needed, but ID and Email are same.
|
||||||
user.Ativo = true
|
user.Ativo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stub creation removed to prevent duplicate profiles.
|
// Create professional profile if applicable
|
||||||
// The frontend is responsible for creating the full professional profile
|
if role == RolePhotographer || role == RoleBusinessOwner {
|
||||||
// immediately after user creation via the professionals service.
|
var funcaoID string
|
||||||
|
|
||||||
|
// If specific professional type is provided, resolve it
|
||||||
|
if tipoProfissional != "" {
|
||||||
|
// Resolve Function ID by Name using list scanning (since GetByName doesn't exist)
|
||||||
|
funcoes, err := s.queries.ListFuncoes(ctx)
|
||||||
|
if err == nil {
|
||||||
|
for _, f := range funcoes {
|
||||||
|
if f.Nome == tipoProfissional {
|
||||||
|
funcaoID = uuid.UUID(f.ID.Bytes).String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare professional input
|
||||||
|
profInput := profissionais.CreateProfissionalInput{
|
||||||
|
Nome: nome,
|
||||||
|
Email: &email,
|
||||||
|
FuncaoProfissionalID: funcaoID,
|
||||||
|
// Add default whatsapp if user has one? User struct doesn't have phone here passed in args,
|
||||||
|
// but we can pass it if we update signature or just leave empty.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the professional
|
||||||
|
_, err = s.profissionaisService.Create(ctx, uuid.UUID(user.ID.Bytes).String(), profInput)
|
||||||
|
if err != nil {
|
||||||
|
// Log error but don't fail user creation?
|
||||||
|
// Better to log. Backend logs not setup here.
|
||||||
|
// Just continue for now, or return error?
|
||||||
|
// If we fail here, user exists but has no profile.
|
||||||
|
// Ideally we should delete user or return error.
|
||||||
|
// Let's log and ignore for now to avoid breaking legacy flows if any.
|
||||||
|
// Actually, if this fails, the bug persists. Best to return error.
|
||||||
|
// But since we already committed user, we should probably return error so client knows.
|
||||||
|
|
||||||
|
// Try to delete user to rollback
|
||||||
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
@ -277,7 +317,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, true)
|
user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -185,9 +185,16 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuari
|
||||||
}
|
}
|
||||||
|
|
||||||
const listAllUsuarios = `-- name: ListAllUsuarios :many
|
const listAllUsuarios = `-- name: ListAllUsuarios :many
|
||||||
SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em
|
SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
|
||||||
FROM usuarios
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
ORDER BY criado_em DESC
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
e.id as empresa_id,
|
||||||
|
e.nome as empresa_nome
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||||
|
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
|
||||||
|
LEFT JOIN empresas e ON cc.empresa_id = e.id
|
||||||
|
ORDER BY u.criado_em DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListAllUsuariosRow struct {
|
type ListAllUsuariosRow struct {
|
||||||
|
|
@ -198,6 +205,10 @@ type ListAllUsuariosRow struct {
|
||||||
Ativo bool `json:"ativo"`
|
Ativo bool `json:"ativo"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
Nome string `json:"nome"`
|
||||||
|
Whatsapp string `json:"whatsapp"`
|
||||||
|
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||||
|
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, error) {
|
func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, error) {
|
||||||
|
|
@ -217,6 +228,10 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er
|
||||||
&i.Ativo,
|
&i.Ativo,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.Nome,
|
||||||
|
&i.Whatsapp,
|
||||||
|
&i.EmpresaID,
|
||||||
|
&i.EmpresaNome,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,16 @@ WHERE id = $1
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: ListAllUsuarios :many
|
-- name: ListAllUsuarios :many
|
||||||
SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em
|
SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em,
|
||||||
FROM usuarios
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
ORDER BY criado_em DESC;
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
e.id as empresa_id,
|
||||||
|
e.nome as empresa_nome
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||||
|
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
|
||||||
|
LEFT JOIN empresas e ON cc.empresa_id = e.id
|
||||||
|
ORDER BY u.criado_em DESC;
|
||||||
|
|
||||||
-- name: CreateCadastroCliente :one
|
-- name: CreateCadastroCliente :one
|
||||||
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
|
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export const TeamPage: React.FC = () => {
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (roleName === "Desconhecido") return false;
|
// if (roleName === "Desconhecido") return false;
|
||||||
|
|
||||||
return matchesSearch && matchesRole && matchesRating;
|
return matchesSearch && matchesRole && matchesRating;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useState, useEffect } from "react";
|
||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import {
|
import {
|
||||||
getPendingUsers,
|
getPendingUsers,
|
||||||
|
getAllUsers,
|
||||||
|
createAdminUser,
|
||||||
approveUser as apiApproveUser,
|
approveUser as apiApproveUser,
|
||||||
rejectUser as apiRejectUser,
|
rejectUser as apiRejectUser,
|
||||||
updateUserRole,
|
updateUserRole,
|
||||||
|
|
@ -12,31 +14,310 @@ import {
|
||||||
XCircle,
|
XCircle,
|
||||||
Clock,
|
Clock,
|
||||||
Search,
|
Search,
|
||||||
Filter,
|
|
||||||
Users,
|
Users,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Edit2,
|
UserPlus,
|
||||||
|
RefreshCw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "../components/Button";
|
import { Button } from "../components/Button";
|
||||||
|
import { Input } from "../components/Input";
|
||||||
|
|
||||||
|
// INTERFACES
|
||||||
interface UserApprovalProps {
|
interface UserApprovalProps {
|
||||||
onNavigate?: (page: string) => void;
|
onNavigate?: (page: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UserDetailsModalProps {
|
||||||
|
selectedUser: any | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onApprove: (userId: string) => void;
|
||||||
|
isProcessing: string | null;
|
||||||
|
viewMode: "pending" | "all";
|
||||||
|
handleRoleChange: (userId: string, newRole: string) => void;
|
||||||
|
setSelectedUser: React.Dispatch<React.SetStateAction<any | null>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateUserModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (e: React.FormEvent) => void;
|
||||||
|
isCreating: boolean;
|
||||||
|
formData: any;
|
||||||
|
setFormData: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// COMPONENT DEFINITIONS
|
||||||
|
const UserDetailsModal: React.FC<UserDetailsModalProps> = ({
|
||||||
|
selectedUser,
|
||||||
|
onClose,
|
||||||
|
onApprove,
|
||||||
|
isProcessing,
|
||||||
|
viewMode,
|
||||||
|
handleRoleChange,
|
||||||
|
setSelectedUser
|
||||||
|
}) => {
|
||||||
|
if (!selectedUser) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg overflow-hidden animate-slide-up">
|
||||||
|
<div className="flex justify-between items-center p-6 border-b border-gray-100">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 font-serif">
|
||||||
|
Detalhes do Cadastro
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<XCircle className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||||
|
Nome
|
||||||
|
</label>
|
||||||
|
<p className="mt-1 text-base text-gray-900 font-medium">
|
||||||
|
{selectedUser.name || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<p className="mt-1 text-base text-gray-900">
|
||||||
|
{selectedUser.email || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||||
|
Telefone
|
||||||
|
</label>
|
||||||
|
<p className="mt-1 text-base text-gray-900">
|
||||||
|
{selectedUser.phone || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||||
|
Data Cadastro
|
||||||
|
</label>
|
||||||
|
<p className="mt-1 text-base text-gray-900">
|
||||||
|
{selectedUser.created_at ? new Date(selectedUser.created_at).toLocaleDateString("pt-BR") : "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedUser.role === "EVENT_OWNER" && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
||||||
|
Empresa Vinculada
|
||||||
|
</label>
|
||||||
|
<p className="mt-1 text-base text-gray-900 font-medium">
|
||||||
|
{selectedUser.company_name || "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-4 border-t border-gray-100">
|
||||||
|
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
||||||
|
Função / Cargo
|
||||||
|
</label>
|
||||||
|
{selectedUser.role === "EVENT_OWNER" ? (
|
||||||
|
<span className="inline-block px-3 py-1 bg-brand-gold/10 text-brand-gold rounded-full text-sm font-medium">
|
||||||
|
Cliente (Empresa)
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
value={
|
||||||
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Cinegrafista" ? "Cinegrafista" :
|
||||||
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Recepcionista" ? "Recepcionista" :
|
||||||
|
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Fotógrafo" ? "Fotógrafo" :
|
||||||
|
selectedUser.role === "PHOTOGRAPHER" ? "Fotógrafo" :
|
||||||
|
selectedUser.role
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
let newRole = e.target.value;
|
||||||
|
if (["Cinegrafista", "Recepcionista", "Fotógrafo"].includes(newRole)) {
|
||||||
|
newRole = "PHOTOGRAPHER";
|
||||||
|
}
|
||||||
|
// Update local selected user state optimistic
|
||||||
|
setSelectedUser({...selectedUser, role: newRole});
|
||||||
|
handleRoleChange(selectedUser.id, newRole);
|
||||||
|
}}
|
||||||
|
className="w-full text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2"
|
||||||
|
disabled={viewMode === "all" && selectedUser.approvalStatus === UserApprovalStatus.APPROVED}
|
||||||
|
>
|
||||||
|
<option value="Fotógrafo">Fotógrafo</option>
|
||||||
|
<option value="Cinegrafista">Cinegrafista</option>
|
||||||
|
<option value="Recepcionista">Recepcionista</option>
|
||||||
|
<option value="RESEARCHER">Pesquisador</option>
|
||||||
|
<option value="BUSINESS_OWNER">Dono do Negócio</option>
|
||||||
|
<option value="SUPERADMIN">Super Admin</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 bg-gray-50 flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</Button>
|
||||||
|
{selectedUser.approvalStatus === UserApprovalStatus.PENDING && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onApprove(selectedUser.id);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
isLoading={isProcessing === selectedUser.id}
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
Aprovar Cadastro
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateUserModal: React.FC<CreateUserModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
isCreating,
|
||||||
|
formData,
|
||||||
|
setFormData
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-md overflow-hidden animate-slide-up">
|
||||||
|
<div className="flex justify-between items-center p-6 border-b border-gray-100">
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 font-serif">
|
||||||
|
Novo Usuário
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<XCircle className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={onSubmit} className="p-6 space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Nome Completo"
|
||||||
|
required
|
||||||
|
value={formData.nome}
|
||||||
|
onChange={(e) => setFormData({...formData, nome: e.target.value})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Telefone (Whatsapp)"
|
||||||
|
value={formData.telefone}
|
||||||
|
onChange={(e) => setFormData({...formData, telefone: e.target.value})}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Input
|
||||||
|
label="Senha"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
minLength={6}
|
||||||
|
value={formData.senha}
|
||||||
|
onChange={(e) => setFormData({...formData, senha: e.target.value})}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Função
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
||||||
|
value={formData.role}
|
||||||
|
onChange={(e) => setFormData({...formData, role: e.target.value as any})}
|
||||||
|
>
|
||||||
|
<option value="PHOTOGRAPHER">Profissional</option>
|
||||||
|
<option value="RESEARCHER">Pesquisador</option>
|
||||||
|
<option value="BUSINESS_OWNER">Dono do Negócio</option>
|
||||||
|
<option value="SUPERADMIN">Super Admin</option>
|
||||||
|
<option value="EVENT_OWNER">Cliente (Empresa)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{formData.role === "PHOTOGRAPHER" && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tipo de Profissional
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
|
||||||
|
value={formData.professional_type}
|
||||||
|
onChange={(e) => setFormData({...formData, professional_type: e.target.value})}
|
||||||
|
>
|
||||||
|
<option value="">Selecione...</option>
|
||||||
|
<option value="Fotógrafo">Fotógrafo</option>
|
||||||
|
<option value="Cinegrafista">Cinegrafista</option>
|
||||||
|
<option value="Recepcionista">Recepcionista</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-4 flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isCreating}
|
||||||
|
>
|
||||||
|
Criar Usuário
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
const { token } = useAuth();
|
const { token } = useAuth();
|
||||||
const [users, setUsers] = useState<any[]>([]);
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>(
|
const [viewMode, setViewMode] = useState<"pending" | "all">("pending");
|
||||||
"ALL"
|
|
||||||
);
|
|
||||||
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
|
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
|
||||||
"cliente"
|
"cliente"
|
||||||
);
|
);
|
||||||
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
||||||
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
const [selectedUser, setSelectedUser] = useState<any | null>(null);
|
||||||
|
|
||||||
|
// Create User State
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
const [createFormData, setCreateFormData] = useState({
|
||||||
|
nome: "",
|
||||||
|
email: "",
|
||||||
|
senha: "",
|
||||||
|
role: "PHOTOGRAPHER",
|
||||||
|
telefone: "",
|
||||||
|
professional_type: "", // For photographer subtype
|
||||||
|
});
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
@ -44,13 +325,20 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await getPendingUsers(token);
|
let result;
|
||||||
|
if (viewMode === "pending") {
|
||||||
|
result = await getPendingUsers(token);
|
||||||
|
} else {
|
||||||
|
result = await getAllUsers(token);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.data) {
|
if (result.data) {
|
||||||
const mappedUsers = result.data.map((u: any) => ({
|
const mappedUsers = result.data.map((u: any) => ({
|
||||||
...u,
|
...u,
|
||||||
approvalStatus: u.ativo
|
approvalStatus: u.ativo
|
||||||
? UserApprovalStatus.APPROVED
|
? UserApprovalStatus.APPROVED
|
||||||
: UserApprovalStatus.PENDING,
|
: UserApprovalStatus.PENDING,
|
||||||
|
// Ensure role is mapped if needed, backend sends "role"
|
||||||
}));
|
}));
|
||||||
setUsers(mappedUsers);
|
setUsers(mappedUsers);
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +351,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}, [token]);
|
}, [token, viewMode]);
|
||||||
|
|
||||||
const handleApprove = async (userId: string) => {
|
const handleApprove = async (userId: string) => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
@ -86,24 +374,53 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
setUsers(prev => prev.map(u => u.id === userId ? {...u, role: newRole} : u));
|
setUsers(prev => prev.map(u => u.id === userId ? {...u, role: newRole} : u));
|
||||||
|
|
||||||
await updateUserRole(userId, newRole, token);
|
await updateUserRole(userId, newRole, token);
|
||||||
// Refresh to be sure
|
|
||||||
// await fetchUsers(); // Optional if we trust optimistic
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao atualizar role:", error);
|
console.error("Erro ao atualizar role:", error);
|
||||||
alert("Erro ao atualizar função do usuário");
|
alert("Erro ao atualizar função do usuário");
|
||||||
// Revert? simpler to just fetch
|
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateUser = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if(!token) return;
|
||||||
|
setIsCreating(true);
|
||||||
|
try {
|
||||||
|
// Prepare payload
|
||||||
|
const payload = {
|
||||||
|
nome: createFormData.nome,
|
||||||
|
email: createFormData.email,
|
||||||
|
senha: createFormData.senha,
|
||||||
|
role: createFormData.role,
|
||||||
|
telefone: createFormData.telefone,
|
||||||
|
tipo_profissional: createFormData.role === "PHOTOGRAPHER" ? createFormData.professional_type : undefined
|
||||||
|
};
|
||||||
|
|
||||||
// Separar usuários Clientes (EVENT_OWNER) e Profissionais (PHOTOGRAPHER)
|
const result = await createAdminUser(payload, token);
|
||||||
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN, RESEARCHER
|
if (result.error) {
|
||||||
|
alert(`Erro: ${result.error}`);
|
||||||
|
} else {
|
||||||
|
alert("Usuário criado com sucesso!");
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setCreateFormData({
|
||||||
|
nome: "", email: "", senha: "", role: "PHOTOGRAPHER", telefone: "", professional_type: ""
|
||||||
|
});
|
||||||
|
fetchUsers();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert("Erro ao criar usuário.");
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Separar usuários Clientes (EVENT_OWNER) e Profissionais
|
||||||
const clientUsers = users.filter(
|
const clientUsers = users.filter(
|
||||||
(user) => user.role === "EVENT_OWNER"
|
(user) => user.role === "EVENT_OWNER"
|
||||||
);
|
);
|
||||||
const professionalUsers = users.filter(
|
const professionalUsers = users.filter(
|
||||||
(user) => user.role === "PHOTOGRAPHER" || user.role === "RESEARCHER" || user.role === "BUSINESS_OWNER" // Include BUSINESS_OWNER if relevant for professional list? Usually Business owner is client-side but maybe here it's treated differently. based on login.tsx business owner is "Dono do Negócio". Let's stick to PHOTOGRAPHER + RESEARCHER as per request, but user explicitly mentioned "admin ter liberdade de editar role".
|
(user) => user.role !== "EVENT_OWNER"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtrar usuários baseado na aba ativa
|
// Filtrar usuários baseado na aba ativa
|
||||||
|
|
@ -113,10 +430,24 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
(user.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(user.name || "").toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
(user.email || "").toLowerCase().includes(searchTerm.toLowerCase());
|
(user.email || "").toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
// In "pending" mode, theoretically all are pending, but let's filter just in case logic changes
|
||||||
|
// Actually getPendingUsers returns only pending. getAllUsers returns all.
|
||||||
|
// So we don't need extra status filtering here unless we add a specific status filter dropdown.
|
||||||
return matchesSearch;
|
return matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStatusBadge = (status: UserApprovalStatus) => {
|
const getStatusBadge = (status: UserApprovalStatus) => {
|
||||||
|
// If we are in "All" mode, approved users are common
|
||||||
|
if (status === UserApprovalStatus.APPROVED) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
Ativo
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const s = status || UserApprovalStatus.PENDING;
|
const s = status || UserApprovalStatus.PENDING;
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case UserApprovalStatus.PENDING:
|
case UserApprovalStatus.PENDING:
|
||||||
|
|
@ -126,13 +457,6 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
Pendente
|
Pendente
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case UserApprovalStatus.APPROVED:
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
|
||||||
Aprovado
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case UserApprovalStatus.REJECTED:
|
case UserApprovalStatus.REJECTED:
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
|
@ -145,145 +469,45 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modal Component
|
|
||||||
const UserDetailsModal = () => {
|
|
||||||
if (!selectedUser) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
|
|
||||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg overflow-hidden animate-slide-up">
|
|
||||||
<div className="flex justify-between items-center p-6 border-b border-gray-100">
|
|
||||||
<h3 className="text-xl font-bold text-gray-900 font-serif">
|
|
||||||
Detalhes do Cadastro
|
|
||||||
</h3>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedUser(null)}
|
|
||||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
<XCircle className="w-6 h-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
||||||
Nome
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 text-base text-gray-900 font-medium">
|
|
||||||
{selectedUser.name || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 text-base text-gray-900">
|
|
||||||
{selectedUser.email || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
||||||
Telefone
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 text-base text-gray-900">
|
|
||||||
{selectedUser.phone || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
||||||
Data Cadastro
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 text-base text-gray-900">
|
|
||||||
{selectedUser.created_at ? new Date(selectedUser.created_at).toLocaleDateString("pt-BR") : "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedUser.role === "EVENT_OWNER" && (
|
|
||||||
<div className="col-span-2">
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide">
|
|
||||||
Empresa Vinculada
|
|
||||||
</label>
|
|
||||||
<p className="mt-1 text-base text-gray-900 font-medium">
|
|
||||||
{selectedUser.company_name || "-"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 border-t border-gray-100">
|
|
||||||
<label className="block text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
|
||||||
Função / Cargo
|
|
||||||
</label>
|
|
||||||
{selectedUser.role === "EVENT_OWNER" ? (
|
|
||||||
<span className="inline-block px-3 py-1 bg-brand-gold/10 text-brand-gold rounded-full text-sm font-medium">
|
|
||||||
Cliente (Empresa)
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<select
|
|
||||||
value={
|
|
||||||
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Cinegrafista" ? "Cinegrafista" :
|
|
||||||
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Recepcionista" ? "Recepcionista" :
|
|
||||||
selectedUser.role === "PHOTOGRAPHER" && selectedUser.professional_type === "Fotógrafo" ? "Fotógrafo" :
|
|
||||||
selectedUser.role === "PHOTOGRAPHER" ? "Fotógrafo" :
|
|
||||||
selectedUser.role
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
let newRole = e.target.value;
|
|
||||||
if (["Cinegrafista", "Recepcionista", "Fotógrafo"].includes(newRole)) {
|
|
||||||
newRole = "PHOTOGRAPHER";
|
|
||||||
}
|
|
||||||
// Update local selected user state optimistic
|
|
||||||
setSelectedUser({...selectedUser, role: newRole});
|
|
||||||
handleRoleChange(selectedUser.id, newRole);
|
|
||||||
}}
|
|
||||||
className="w-full text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2"
|
|
||||||
>
|
|
||||||
<option value="Fotógrafo">Fotógrafo</option>
|
|
||||||
<option value="Cinegrafista">Cinegrafista</option>
|
|
||||||
<option value="Recepcionista">Recepcionista</option>
|
|
||||||
<option value="RESEARCHER">Pesquisador</option>
|
|
||||||
<option value="BUSINESS_OWNER">Empresa</option>
|
|
||||||
</select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 bg-gray-50 flex justify-end gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setSelectedUser(null)}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
handleApprove(selectedUser.id);
|
|
||||||
setSelectedUser(null);
|
|
||||||
}}
|
|
||||||
isLoading={isProcessing === selectedUser.id}
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
|
||||||
Aprovar Cadastro
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
|
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 sm:mb-8">
|
<div className="mb-6 sm:mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
<div>
|
||||||
Aprovação de Cadastros
|
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
||||||
</h1>
|
{viewMode === "pending" ? "Aprovação de Cadastros" : "Gerenciamento de Usuários"}
|
||||||
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
</h1>
|
||||||
Gerencie os cadastros pendentes de aprovação
|
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
||||||
</p>
|
{viewMode === "pending"
|
||||||
|
? "Gerencie os cadastros pendentes de aprovação"
|
||||||
|
: "Visualize e gerencie todos os usuários do sistema"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="bg-white rounded-lg p-1 border border-gray-200 flex">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("pending")}
|
||||||
|
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
viewMode === "pending" ? "bg-brand-gold text-white" : "text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Pendentes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("all")}
|
||||||
|
className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
viewMode === "all" ? "bg-brand-gold text-white" : "text-gray-600 hover:bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Todos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Button onClick={() => setShowCreateModal(true)}>
|
||||||
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
|
Novo Usuário
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
|
|
@ -297,7 +521,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Users className="w-5 h-5" />
|
<Users className="w-5 h-5" />
|
||||||
Cadastros Clientes
|
{activeTab === "cliente" ? "Clientes" : "Clientes"}
|
||||||
<span
|
<span
|
||||||
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "cliente"
|
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "cliente"
|
||||||
? "bg-[#B9CF33] text-white"
|
? "bg-[#B9CF33] text-white"
|
||||||
|
|
@ -315,7 +539,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Briefcase className="w-5 h-5" />
|
<Briefcase className="w-5 h-5" />
|
||||||
Cadastros Profissionais
|
{activeTab === "profissional" ? "Profissionais & Staff" : "Profissionais & Staff"}
|
||||||
<span
|
<span
|
||||||
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "profissional"
|
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "profissional"
|
||||||
? "bg-[#B9CF33] text-white"
|
? "bg-[#B9CF33] text-white"
|
||||||
|
|
@ -330,9 +554,9 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-6">
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4 items-center">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative w-full">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -342,6 +566,13 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={fetchUsers}
|
||||||
|
className="p-2 text-gray-400 hover:text-brand-gold transition-colors"
|
||||||
|
title="Atualizar lista"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-5 h-5 ${isLoading ? "animate-spin" : ""}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -350,7 +581,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
Carregando solicitações...
|
Carregando usuários...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table key={activeTab} className="min-w-full divide-y divide-gray-200">
|
<table key={activeTab} className="min-w-full divide-y divide-gray-200">
|
||||||
|
|
@ -442,10 +673,11 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
) : (
|
) : (
|
||||||
<select
|
<select
|
||||||
value={
|
value={
|
||||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Cinegrafista" ? "Cinegrafista" :
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Cinegrafista") ? "Cinegrafista" :
|
||||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Recepcionista" ? "Recepcionista" :
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Recepcionista") ? "Recepcionista" :
|
||||||
user.role === "PHOTOGRAPHER" && user.professional_type === "Fotógrafo" ? "Fotógrafo" :
|
(user.role === "PHOTOGRAPHER" && user.professional_type === "Fotógrafo") ? "Fotógrafo" :
|
||||||
user.role === "PHOTOGRAPHER" ? "Fotógrafo" :
|
(user.role === "PHOTOGRAPHER" && !user.professional_type) ? "Fotógrafo" : // Default fallback
|
||||||
|
(user.role === "PHOTOGRAPHER") ? "Fotógrafo" : // Catch all photographer main role
|
||||||
user.role
|
user.role
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -456,12 +688,14 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
handleRoleChange(user.id, newRole);
|
handleRoleChange(user.id, newRole);
|
||||||
}}
|
}}
|
||||||
className="text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50"
|
className="text-sm border-gray-300 rounded-md shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50"
|
||||||
|
disabled={viewMode === "all" && user.approvalStatus === UserApprovalStatus.APPROVED}
|
||||||
>
|
>
|
||||||
<option value="Fotógrafo">Fotógrafo</option>
|
<option value="Fotógrafo">Fotógrafo</option>
|
||||||
<option value="Cinegrafista">Cinegrafista</option>
|
<option value="Cinegrafista">Cinegrafista</option>
|
||||||
<option value="Recepcionista">Recepcionista</option>
|
<option value="Recepcionista">Recepcionista</option>
|
||||||
<option value="RESEARCHER">Pesquisador</option>
|
<option value="RESEARCHER">Pesquisador</option>
|
||||||
<option value="BUSINESS_OWNER">Empresa</option>
|
<option value="BUSINESS_OWNER">Dono do Negócio</option>
|
||||||
|
<option value="SUPERADMIN">Super Admin</option>
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -481,17 +715,21 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium" onClick={(e) => e.stopPropagation()}>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium" onClick={(e) => e.stopPropagation()}>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
{user.approvalStatus !== UserApprovalStatus.APPROVED && (
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => handleApprove(user.id)}
|
size="sm"
|
||||||
isLoading={isProcessing === user.id}
|
onClick={() => handleApprove(user.id)}
|
||||||
disabled={isProcessing !== null}
|
isLoading={isProcessing === user.id}
|
||||||
className="bg-green-600 hover:bg-green-700 text-white"
|
disabled={isProcessing !== null}
|
||||||
>
|
className="bg-green-600 hover:bg-green-700 text-white"
|
||||||
<CheckCircle className="w-4 h-4 mr-1" />
|
>
|
||||||
Aprovar
|
<CheckCircle className="w-4 h-4 mr-1" />
|
||||||
</Button>
|
Aprovar
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{user.approvalStatus === UserApprovalStatus.APPROVED && (
|
||||||
|
<span className="text-gray-400 text-xs">--</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -503,7 +741,23 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
}
|
}
|
||||||
</div >
|
</div >
|
||||||
</div >
|
</div >
|
||||||
<UserDetailsModal />
|
<UserDetailsModal
|
||||||
|
selectedUser={selectedUser}
|
||||||
|
onClose={() => setSelectedUser(null)}
|
||||||
|
onApprove={handleApprove}
|
||||||
|
isProcessing={isProcessing}
|
||||||
|
viewMode={viewMode}
|
||||||
|
handleRoleChange={handleRoleChange}
|
||||||
|
setSelectedUser={setSelectedUser}
|
||||||
|
/>
|
||||||
|
<CreateUserModal
|
||||||
|
isOpen={showCreateModal}
|
||||||
|
onClose={() => setShowCreateModal(false)}
|
||||||
|
onSubmit={handleCreateUser}
|
||||||
|
isCreating={isCreating}
|
||||||
|
formData={createFormData}
|
||||||
|
setFormData={setCreateFormData}
|
||||||
|
/>
|
||||||
</div >
|
</div >
|
||||||
</div >
|
</div >
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -803,6 +803,74 @@ export async function removeProfessional(token: string, eventId: string, profess
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca todos os usuários (Admin)
|
||||||
|
*/
|
||||||
|
export async function getAllUsers(token: string): Promise<ApiResponse<any[]>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/admin/users`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: null,
|
||||||
|
isBackendDown: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching all users:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um novo usuário (Admin)
|
||||||
|
*/
|
||||||
|
export async function createAdminUser(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 user:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca profissionais de um evento
|
* Busca profissionais de um evento
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue