diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index d220a32..7d43701 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -482,7 +482,7 @@ func (h *Handler) AdminCreateUser(c *gin.Context) { } // 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 strings.Contains(err.Error(), "duplicate key") { 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)) for i, u := range users { + empresaId := "" + if u.EmpresaID.Valid { + empresaId = uuid.UUID(u.EmpresaID.Bytes).String() + } + resp[i] = map[string]interface{}{ - "id": uuid.UUID(u.ID.Bytes).String(), - "email": u.Email, - "role": u.Role, - "ativo": u.Ativo, - "created_at": u.CriadoEm.Time, + "id": uuid.UUID(u.ID.Bytes).String(), + "email": u.Email, + "role": u.Role, + "ativo": u.Ativo, + "created_at": u.CriadoEm.Time, + "name": u.Nome, + "phone": u.Whatsapp, + "company_name": u.EmpresaNome.String, + "company_id": empresaId, + "professional_type": u.TipoProfissional.String, } } diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 6a4a4c5..cd8358b 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -196,7 +196,7 @@ func (s *Service) ApproveUser(ctx context.Context, id string) error { 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 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost) if err != nil { @@ -205,9 +205,10 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome // Create user user, err := s.queries.CreateUsuario(ctx, generated.CreateUsuarioParams{ - Email: email, - SenhaHash: string(hashedPassword), - Role: role, + Email: email, + SenhaHash: string(hashedPassword), + Role: role, + TipoProfissional: toPgText(&tipoProfissional), }) if err != nil { return nil, err @@ -217,17 +218,56 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome // Approve user immediately err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String()) if err != nil { - // Log error but don't fail user creation? Or fail? - // Better to return error return nil, err } // 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. + // Create professional profile if applicable + if role == RolePhotographer || role == RoleBusinessOwner { + 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 } @@ -277,7 +317,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error { existingUser, err := s.queries.GetUsuarioByEmail(ctx, u.Email) if err != nil { // 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 { return err } diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go index 1aaf300..b1bff79 100644 --- a/backend/internal/db/generated/usuarios.sql.go +++ b/backend/internal/db/generated/usuarios.sql.go @@ -185,9 +185,16 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuari } const listAllUsuarios = `-- name: ListAllUsuarios :many -SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em -FROM usuarios -ORDER BY criado_em DESC +SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em, + COALESCE(cp.nome, cc.nome, '') as nome, + 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 { @@ -198,6 +205,10 @@ type ListAllUsuariosRow struct { Ativo bool `json:"ativo"` CriadoEm pgtype.Timestamptz `json:"criado_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) { @@ -217,6 +228,10 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er &i.Ativo, &i.CriadoEm, &i.AtualizadoEm, + &i.Nome, + &i.Whatsapp, + &i.EmpresaID, + &i.EmpresaNome, ); err != nil { return nil, err } diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql index 6e8aa34..4654898 100644 --- a/backend/internal/db/queries/usuarios.sql +++ b/backend/internal/db/queries/usuarios.sql @@ -56,9 +56,16 @@ WHERE id = $1 RETURNING *; -- name: ListAllUsuarios :many -SELECT id, email, role, tipo_profissional, ativo, criado_em, atualizado_em -FROM usuarios -ORDER BY criado_em DESC; +SELECT u.id, u.email, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em, + COALESCE(cp.nome, cc.nome, '') as nome, + 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 INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone) diff --git a/frontend/pages/Team.tsx b/frontend/pages/Team.tsx index bc34b33..6aa2c30 100644 --- a/frontend/pages/Team.tsx +++ b/frontend/pages/Team.tsx @@ -131,7 +131,7 @@ export const TeamPage: React.FC = () => { } })(); - if (roleName === "Desconhecido") return false; + // if (roleName === "Desconhecido") return false; return matchesSearch && matchesRole && matchesRating; }); diff --git a/frontend/pages/UserApproval.tsx b/frontend/pages/UserApproval.tsx index 8d444e9..e6d387f 100644 --- a/frontend/pages/UserApproval.tsx +++ b/frontend/pages/UserApproval.tsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from "react"; import { useAuth } from "../contexts/AuthContext"; import { getPendingUsers, + getAllUsers, + createAdminUser, approveUser as apiApproveUser, rejectUser as apiRejectUser, updateUserRole, @@ -12,30 +14,309 @@ import { XCircle, Clock, Search, - Filter, Users, Briefcase, - Edit2, + UserPlus, + RefreshCw, } from "lucide-react"; import { Button } from "../components/Button"; +import { Input } from "../components/Input"; +// INTERFACES interface UserApprovalProps { 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>; +} + +interface CreateUserModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (e: React.FormEvent) => void; + isCreating: boolean; + formData: any; + setFormData: React.Dispatch>; +} + +// COMPONENT DEFINITIONS +const UserDetailsModal: React.FC = ({ + selectedUser, + onClose, + onApprove, + isProcessing, + viewMode, + handleRoleChange, + setSelectedUser +}) => { + if (!selectedUser) return null; + + return ( +
+
+
+

+ Detalhes do Cadastro +

+ +
+ +
+
+
+ +

+ {selectedUser.name || "-"} +

+
+
+ +

+ {selectedUser.email || "-"} +

+
+ +
+ +

+ {selectedUser.phone || "-"} +

+
+
+ +

+ {selectedUser.created_at ? new Date(selectedUser.created_at).toLocaleDateString("pt-BR") : "-"} +

+
+ + {selectedUser.role === "EVENT_OWNER" && ( +
+ +

+ {selectedUser.company_name || "-"} +

+
+ )} +
+ +
+ + {selectedUser.role === "EVENT_OWNER" ? ( + + Cliente (Empresa) + + ) : ( + + )} +
+
+ +
+ + {selectedUser.approvalStatus === UserApprovalStatus.PENDING && ( + + )} +
+
+
+ ); +}; + +const CreateUserModal: React.FC = ({ + isOpen, + onClose, + onSubmit, + isCreating, + formData, + setFormData +}) => { + if (!isOpen) return null; + return ( +
+
+
+

+ Novo Usuário +

+ +
+
+ setFormData({...formData, nome: e.target.value})} + /> + setFormData({...formData, email: e.target.value})} + /> + setFormData({...formData, telefone: e.target.value})} + /> +
+ setFormData({...formData, senha: e.target.value})} + /> +
+ + +
+
+ + {formData.role === "PHOTOGRAPHER" && ( +
+ + +
+ )} + +
+ + +
+
+
+
+ ); +}; + export const UserApproval: React.FC = ({ onNavigate }) => { const { token } = useAuth(); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); - const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>( - "ALL" - ); + const [viewMode, setViewMode] = useState<"pending" | "all">("pending"); const [activeTab, setActiveTab] = useState<"cliente" | "profissional">( "cliente" ); const [isProcessing, setIsProcessing] = useState(null); const [selectedUser, setSelectedUser] = useState(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 () => { if (!token) { @@ -44,13 +325,20 @@ export const UserApproval: React.FC = ({ onNavigate }) => { } setIsLoading(true); try { - const result = await getPendingUsers(token); + let result; + if (viewMode === "pending") { + result = await getPendingUsers(token); + } else { + result = await getAllUsers(token); + } + if (result.data) { const mappedUsers = result.data.map((u: any) => ({ ...u, approvalStatus: u.ativo ? UserApprovalStatus.APPROVED : UserApprovalStatus.PENDING, + // Ensure role is mapped if needed, backend sends "role" })); setUsers(mappedUsers); } @@ -63,7 +351,7 @@ export const UserApproval: React.FC = ({ onNavigate }) => { useEffect(() => { fetchUsers(); - }, [token]); + }, [token, viewMode]); const handleApprove = async (userId: string) => { if (!token) return; @@ -86,24 +374,53 @@ export const UserApproval: React.FC = ({ onNavigate }) => { setUsers(prev => prev.map(u => u.id === userId ? {...u, role: newRole} : u)); await updateUserRole(userId, newRole, token); - // Refresh to be sure - // await fetchUsers(); // Optional if we trust optimistic } catch (error) { console.error("Erro ao atualizar role:", error); alert("Erro ao atualizar função do usuário"); - // Revert? simpler to just fetch 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) - // Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN, RESEARCHER + const result = await createAdminUser(payload, token); + 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( (user) => user.role === "EVENT_OWNER" ); 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 @@ -113,10 +430,24 @@ export const UserApproval: React.FC = ({ onNavigate }) => { const matchesSearch = (user.name || "").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; }); const getStatusBadge = (status: UserApprovalStatus) => { + // If we are in "All" mode, approved users are common + if (status === UserApprovalStatus.APPROVED) { + return ( + + + Ativo + + ); + } + const s = status || UserApprovalStatus.PENDING; switch (s) { case UserApprovalStatus.PENDING: @@ -126,13 +457,6 @@ export const UserApproval: React.FC = ({ onNavigate }) => { Pendente ); - case UserApprovalStatus.APPROVED: - return ( - - - Aprovado - - ); case UserApprovalStatus.REJECTED: return ( @@ -145,145 +469,45 @@ export const UserApproval: React.FC = ({ onNavigate }) => { } }; - // Modal Component - const UserDetailsModal = () => { - if (!selectedUser) return null; - - return ( -
-
-
-

- Detalhes do Cadastro -

- -
- -
-
-
- -

- {selectedUser.name || "-"} -

-
-
- -

- {selectedUser.email || "-"} -

-
- -
- -

- {selectedUser.phone || "-"} -

-
-
- -

- {selectedUser.created_at ? new Date(selectedUser.created_at).toLocaleDateString("pt-BR") : "-"} -

-
- - {selectedUser.role === "EVENT_OWNER" && ( -
- -

- {selectedUser.company_name || "-"} -

-
- )} -
- -
- - {selectedUser.role === "EVENT_OWNER" ? ( - - Cliente (Empresa) - - ) : ( - - )} -
-
- -
- - -
-
-
- ); - }; - return (
{/* Header */} -
-

- Aprovação de Cadastros -

-

- Gerencie os cadastros pendentes de aprovação -

+
+
+

+ {viewMode === "pending" ? "Aprovação de Cadastros" : "Gerenciamento de Usuários"} +

+

+ {viewMode === "pending" + ? "Gerencie os cadastros pendentes de aprovação" + : "Visualize e gerencie todos os usuários do sistema"} +

+
+
+
+ + +
+ +
{/* Tabs */} @@ -297,7 +521,7 @@ export const UserApproval: React.FC = ({ onNavigate }) => { }`} > - Cadastros Clientes + {activeTab === "cliente" ? "Clientes" : "Clientes"} = ({ onNavigate }) => { }`} > - Cadastros Profissionais + {activeTab === "profissional" ? "Profissionais & Staff" : "Profissionais & Staff"} = ({ onNavigate }) => { {/* Filters */}
-
+
{/* Search */} -
+
= ({ 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" />
+
@@ -350,7 +581,7 @@ export const UserApproval: React.FC = ({ onNavigate }) => {
{isLoading ? (
- Carregando solicitações... + Carregando usuários...
) : ( @@ -442,10 +673,11 @@ export const UserApproval: React.FC = ({ onNavigate }) => { ) : ( )} @@ -481,17 +715,21 @@ export const UserApproval: React.FC = ({ onNavigate }) => { @@ -503,7 +741,23 @@ export const UserApproval: React.FC = ({ onNavigate }) => { } - + setSelectedUser(null)} + onApprove={handleApprove} + isProcessing={isProcessing} + viewMode={viewMode} + handleRoleChange={handleRoleChange} + setSelectedUser={setSelectedUser} + /> + setShowCreateModal(false)} + onSubmit={handleCreateUser} + isCreating={isCreating} + formData={createFormData} + setFormData={setCreateFormData} + /> ); diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index b8a5792..ce2aea3 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -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> { + 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> { + 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 */
e.stopPropagation()}>
- - + {user.approvalStatus !== UserApprovalStatus.APPROVED && ( + + )} + {user.approvalStatus === UserApprovalStatus.APPROVED && ( + -- + )}