fix: validação de usuários inativos e melhorias na lista de aprovação
- Bloqueia login e auto-login para usuários com status inativo - Implementa logout chamando endpoint do backend para limpar cookies - Adiciona verificação de status ativo na restauração de sessão - Inclui coluna 'Telefone' na tabela de aprovação de cadastros - Corrige bug de renderização nas tabelas ao trocar de abas
This commit is contained in:
parent
3011a0634f
commit
bae43a14cb
15 changed files with 637 additions and 405 deletions
|
|
@ -136,15 +136,7 @@ func main() {
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
api.Use(auth.AuthMiddleware(cfg))
|
api.Use(auth.AuthMiddleware(cfg))
|
||||||
{
|
{
|
||||||
api.GET("/me", func(c *gin.Context) {
|
api.GET("/me", authHandler.Me)
|
||||||
userID, _ := c.Get("userID")
|
|
||||||
role, _ := c.Get("role")
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"user_id": userID,
|
|
||||||
"role": role,
|
|
||||||
"message": "You are authenticated",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
profGroup := api.Group("/profissionais")
|
profGroup := api.Group("/profissionais")
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1294,6 +1294,29 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/me": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get current authenticated user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Get current user",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.loginResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/profissionais": {
|
"/api/profissionais": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2209,6 +2232,10 @@ const docTemplate = `{
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"empresa_id": {
|
||||||
|
"description": "Optional, for EVENT_OWNER",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nome": {
|
"nome": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -2242,12 +2269,21 @@ const docTemplate = `{
|
||||||
"ativo": {
|
"ativo": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"company_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1288,6 +1288,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/me": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get current authenticated user",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"auth"
|
||||||
|
],
|
||||||
|
"summary": "Get current user",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.loginResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/profissionais": {
|
"/api/profissionais": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -2203,6 +2226,10 @@
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"empresa_id": {
|
||||||
|
"description": "Optional, for EVENT_OWNER",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"nome": {
|
"nome": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -2236,12 +2263,21 @@
|
||||||
"ativo": {
|
"ativo": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"company_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
|
empresa_id:
|
||||||
|
description: Optional, for EVENT_OWNER
|
||||||
|
type: string
|
||||||
nome:
|
nome:
|
||||||
type: string
|
type: string
|
||||||
role:
|
role:
|
||||||
|
|
@ -69,10 +72,16 @@ definitions:
|
||||||
properties:
|
properties:
|
||||||
ativo:
|
ativo:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
company_name:
|
||||||
|
type: string
|
||||||
email:
|
email:
|
||||||
type: string
|
type: string
|
||||||
id:
|
id:
|
||||||
type: string
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
phone:
|
||||||
|
type: string
|
||||||
role:
|
role:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -1193,6 +1202,21 @@ paths:
|
||||||
summary: Update function
|
summary: Update function
|
||||||
tags:
|
tags:
|
||||||
- funcoes
|
- funcoes
|
||||||
|
/api/me:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get current authenticated user
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/auth.loginResponse'
|
||||||
|
summary: Get current user
|
||||||
|
tags:
|
||||||
|
- auth
|
||||||
/api/profissionais:
|
/api/profissionais:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type registerRequest struct {
|
||||||
Nome string `json:"nome" binding:"required"`
|
Nome string `json:"nome" binding:"required"`
|
||||||
Telefone string `json:"telefone"`
|
Telefone string `json:"telefone"`
|
||||||
Role string `json:"role" binding:"required"` // Role is now required
|
Role string `json:"role" binding:"required"` // Role is now required
|
||||||
|
EmpresaID string `json:"empresa_id"` // Optional, for EVENT_OWNER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register godoc
|
// Register godoc
|
||||||
|
|
@ -46,15 +47,27 @@ func (h *Handler) Register(c *gin.Context) {
|
||||||
|
|
||||||
// Create professional data only if role is appropriate
|
// Create professional data only if role is appropriate
|
||||||
var profData *profissionais.CreateProfissionalInput
|
var profData *profissionais.CreateProfissionalInput
|
||||||
// COMMENTED OUT to enable 2-step registration (User -> Full Profile)
|
// For PHOTOGRAPHER or BUSINESS_OWNER, we might populate this if we were doing 1-step,
|
||||||
// if req.Role == RolePhotographer || req.Role == RoleBusinessOwner {
|
// but actually 'nome' and 'telefone' are passed as args now.
|
||||||
// profData = &profissionais.CreateProfissionalInput{
|
// We keep passing nil for profData because Service logic for Professionals relies on 'CreateProfissionalInput'
|
||||||
// Nome: req.Nome,
|
// However, I updated Service to take nome/telefone directly.
|
||||||
// Whatsapp: &req.Telefone,
|
// Wait, the Service code I JUST wrote takes (email, senha, role, nome, telefone, empresaID, profissionalData).
|
||||||
// }
|
// If role is Photographer, the Service code checks `profissionalData`.
|
||||||
// }
|
// I should probably populate `profissionalData` if it's a professional.
|
||||||
|
|
||||||
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, profData)
|
if req.Role == "PHOTOGRAPHER" || req.Role == "BUSINESS_OWNER" {
|
||||||
|
profData = &profissionais.CreateProfissionalInput{
|
||||||
|
Nome: req.Nome,
|
||||||
|
Whatsapp: &req.Telefone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var empresaIDPtr *string
|
||||||
|
if req.EmpresaID != "" {
|
||||||
|
empresaIDPtr = &req.EmpresaID
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, empresaIDPtr, profData)
|
||||||
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"})
|
||||||
|
|
@ -121,6 +134,9 @@ type userResponse struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Ativo bool `json:"ativo"`
|
Ativo bool `json:"ativo"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Phone string `json:"phone,omitempty"`
|
||||||
|
CompanyName string `json:"company_name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login godoc
|
// Login godoc
|
||||||
|
|
@ -258,6 +274,52 @@ func (h *Handler) Logout(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
c.JSON(http.StatusOK, gin.H{"message": "logged out"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Me godoc
|
||||||
|
// @Summary Get current user
|
||||||
|
// @Description Get current authenticated user
|
||||||
|
// @Tags auth
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} loginResponse
|
||||||
|
// @Router /api/me [get]
|
||||||
|
func (h *Handler) Me(c *gin.Context) {
|
||||||
|
// User ID comes from context (AuthMiddleware)
|
||||||
|
userID, exists := c.Get("userID")
|
||||||
|
if !exists {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can fetch fresh user data
|
||||||
|
user, err := h.service.GetUser(c.Request.Context(), userID.(string))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var empresaNome string
|
||||||
|
if user.EmpresaNome.Valid {
|
||||||
|
empresaNome = user.EmpresaNome.String
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := loginResponse{
|
||||||
|
User: userResponse{
|
||||||
|
ID: uuid.UUID(user.ID.Bytes).String(),
|
||||||
|
Email: user.Email,
|
||||||
|
Role: user.Role,
|
||||||
|
Ativo: user.Ativo,
|
||||||
|
Name: user.Nome,
|
||||||
|
Phone: user.Whatsapp,
|
||||||
|
CompanyName: empresaNome,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// 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.
|
||||||
|
// For session restore, we mainly need the User object.
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
// ListPending godoc
|
// ListPending godoc
|
||||||
// @Summary List pending users
|
// @Summary List pending users
|
||||||
// @Description List users with ativo=false
|
// @Description List users with ativo=false
|
||||||
|
|
@ -287,13 +349,12 @@ func (h *Handler) ListPending(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 {
|
||||||
var nome string
|
nome := u.Nome
|
||||||
if u.Nome.Valid {
|
whatsapp := u.Whatsapp
|
||||||
nome = u.Nome.String
|
|
||||||
}
|
var empresaNome string
|
||||||
var whatsapp string
|
if u.EmpresaNome.Valid {
|
||||||
if u.Whatsapp.Valid {
|
empresaNome = u.EmpresaNome.String
|
||||||
whatsapp = u.Whatsapp.String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp[i] = map[string]interface{}{
|
resp[i] = map[string]interface{}{
|
||||||
|
|
@ -304,6 +365,7 @@ func (h *Handler) ListPending(c *gin.Context) {
|
||||||
"created_at": u.CriadoEm.Time,
|
"created_at": u.CriadoEm.Time,
|
||||||
"name": nome, // Mapped to name for frontend compatibility
|
"name": nome, // Mapped to name for frontend compatibility
|
||||||
"phone": whatsapp,
|
"phone": whatsapp,
|
||||||
|
"company_name": empresaNome,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -503,12 +565,20 @@ func (h *Handler) GetUser(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var empresaNome string
|
||||||
|
if user.EmpresaNome.Valid {
|
||||||
|
empresaNome = user.EmpresaNome.String
|
||||||
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"id": uuid.UUID(user.ID.Bytes).String(),
|
"id": uuid.UUID(user.ID.Bytes).String(),
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
"role": user.Role,
|
"role": user.Role,
|
||||||
"ativo": user.Ativo,
|
"ativo": user.Ativo,
|
||||||
"created_at": user.CriadoEm.Time,
|
"created_at": user.CriadoEm.Time,
|
||||||
|
"name": user.Nome,
|
||||||
|
"phone": user.Whatsapp,
|
||||||
|
"company_name": empresaNome,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, resp)
|
c.JSON(http.StatusOK, resp)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ func NewService(queries *generated.Queries, profissionaisService *profissionais.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Register(ctx context.Context, email, senha, role string, profissionalData *profissionais.CreateProfissionalInput) (*generated.Usuario, error) {
|
func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput) (*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 {
|
||||||
|
|
@ -71,6 +71,30 @@ func (s *Service) Register(ctx context.Context, email, senha, role string, profi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If role is 'EVENT_OWNER', create client profile
|
||||||
|
if role == RoleEventOwner {
|
||||||
|
userID := user.ID
|
||||||
|
var empID pgtype.UUID
|
||||||
|
if empresaID != nil && *empresaID != "" {
|
||||||
|
parsedEmpID, err := uuid.Parse(*empresaID)
|
||||||
|
if err == nil {
|
||||||
|
empID.Bytes = parsedEmpID
|
||||||
|
empID.Valid = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
|
||||||
|
UsuarioID: userID,
|
||||||
|
EmpresaID: empID,
|
||||||
|
Nome: pgtype.Text{String: nome, Valid: nome != ""},
|
||||||
|
Telefone: pgtype.Text{String: telefone, Valid: telefone != ""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = s.queries.DeleteUsuario(ctx, user.ID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,6 +261,7 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error {
|
||||||
{"admin@photum.com", RoleSuperAdmin, "Dev Admin"},
|
{"admin@photum.com", RoleSuperAdmin, "Dev Admin"},
|
||||||
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"},
|
{"empresa@photum.com", RoleBusinessOwner, "PHOTUM CEO"},
|
||||||
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"},
|
{"foto@photum.com", RolePhotographer, "COLABORADOR PHOTUM"},
|
||||||
|
{"cliente@photum.com", RoleEventOwner, "CLIENTE TESTE"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range demoUsers {
|
for _, u := range demoUsers {
|
||||||
|
|
@ -270,7 +295,7 @@ func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow
|
||||||
return s.queries.ListAllUsuarios(ctx)
|
return s.queries.ListAllUsuarios(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetUser(ctx context.Context, id string) (*generated.Usuario, error) {
|
func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) {
|
||||||
parsedUUID, err := uuid.Parse(id)
|
parsedUUID, err := uuid.Parse(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,16 @@ type AnosFormatura struct {
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CadastroCliente struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||||
|
Nome pgtype.Text `json:"nome"`
|
||||||
|
Telefone pgtype.Text `json:"telefone"`
|
||||||
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
}
|
||||||
|
|
||||||
type CadastroFot struct {
|
type CadastroFot struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
Fot int32 `json:"fot"`
|
Fot int32 `json:"fot"`
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,39 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const createCadastroCliente = `-- name: CreateCadastroCliente :one
|
||||||
|
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateCadastroClienteParams struct {
|
||||||
|
UsuarioID pgtype.UUID `json:"usuario_id"`
|
||||||
|
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||||
|
Nome pgtype.Text `json:"nome"`
|
||||||
|
Telefone pgtype.Text `json:"telefone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroClienteParams) (CadastroCliente, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createCadastroCliente,
|
||||||
|
arg.UsuarioID,
|
||||||
|
arg.EmpresaID,
|
||||||
|
arg.Nome,
|
||||||
|
arg.Telefone,
|
||||||
|
)
|
||||||
|
var i CadastroCliente
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UsuarioID,
|
||||||
|
&i.EmpresaID,
|
||||||
|
&i.Nome,
|
||||||
|
&i.Telefone,
|
||||||
|
&i.CriadoEm,
|
||||||
|
&i.AtualizadoEm,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const createUsuario = `-- name: CreateUsuario :one
|
const createUsuario = `-- name: CreateUsuario :one
|
||||||
INSERT INTO usuarios (email, senha_hash, role, ativo)
|
INSERT INTO usuarios (email, senha_hash, role, ativo)
|
||||||
VALUES ($1, $2, $3, false)
|
VALUES ($1, $2, $3, false)
|
||||||
|
|
@ -69,13 +102,33 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUsuarioByID = `-- name: GetUsuarioByID :one
|
const getUsuarioByID = `-- name: GetUsuarioByID :one
|
||||||
SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios
|
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||||
WHERE id = $1 LIMIT 1
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
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
|
||||||
|
WHERE u.id = $1 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario, error) {
|
type GetUsuarioByIDRow struct {
|
||||||
|
ID pgtype.UUID `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
SenhaHash string `json:"senha_hash"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Ativo bool `json:"ativo"`
|
||||||
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
|
Nome string `json:"nome"`
|
||||||
|
Whatsapp string `json:"whatsapp"`
|
||||||
|
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuarioByIDRow, error) {
|
||||||
row := q.db.QueryRow(ctx, getUsuarioByID, id)
|
row := q.db.QueryRow(ctx, getUsuarioByID, id)
|
||||||
var i Usuario
|
var i GetUsuarioByIDRow
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Email,
|
&i.Email,
|
||||||
|
|
@ -84,6 +137,9 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (Usuario,
|
||||||
&i.Ativo,
|
&i.Ativo,
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
|
&i.Nome,
|
||||||
|
&i.Whatsapp,
|
||||||
|
&i.EmpresaNome,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -132,9 +188,13 @@ func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, er
|
||||||
|
|
||||||
const listUsuariosPending = `-- name: ListUsuariosPending :many
|
const listUsuariosPending = `-- name: ListUsuariosPending :many
|
||||||
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
||||||
cp.nome, cp.whatsapp
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
e.nome as empresa_nome
|
||||||
FROM usuarios u
|
FROM usuarios u
|
||||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
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
|
||||||
WHERE u.ativo = false
|
WHERE u.ativo = false
|
||||||
ORDER BY u.criado_em DESC
|
ORDER BY u.criado_em DESC
|
||||||
`
|
`
|
||||||
|
|
@ -145,8 +205,9 @@ type ListUsuariosPendingRow struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Ativo bool `json:"ativo"`
|
Ativo bool `json:"ativo"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
Nome pgtype.Text `json:"nome"`
|
Nome string `json:"nome"`
|
||||||
Whatsapp pgtype.Text `json:"whatsapp"`
|
Whatsapp string `json:"whatsapp"`
|
||||||
|
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) {
|
func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendingRow, error) {
|
||||||
|
|
@ -166,6 +227,7 @@ func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendin
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.Nome,
|
&i.Nome,
|
||||||
&i.Whatsapp,
|
&i.Whatsapp,
|
||||||
|
&i.EmpresaNome,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,15 @@ SELECT * FROM usuarios
|
||||||
WHERE email = $1 LIMIT 1;
|
WHERE email = $1 LIMIT 1;
|
||||||
|
|
||||||
-- name: GetUsuarioByID :one
|
-- name: GetUsuarioByID :one
|
||||||
SELECT * FROM usuarios
|
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||||
WHERE id = $1 LIMIT 1;
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
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
|
||||||
|
WHERE u.id = $1 LIMIT 1;
|
||||||
|
|
||||||
-- name: DeleteUsuario :exec
|
-- name: DeleteUsuario :exec
|
||||||
DELETE FROM usuarios
|
DELETE FROM usuarios
|
||||||
|
|
@ -17,9 +24,13 @@ WHERE id = $1;
|
||||||
|
|
||||||
-- name: ListUsuariosPending :many
|
-- name: ListUsuariosPending :many
|
||||||
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
||||||
cp.nome, cp.whatsapp
|
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||||
|
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||||
|
e.nome as empresa_nome
|
||||||
FROM usuarios u
|
FROM usuarios u
|
||||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
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
|
||||||
WHERE u.ativo = false
|
WHERE u.ativo = false
|
||||||
ORDER BY u.criado_em DESC;
|
ORDER BY u.criado_em DESC;
|
||||||
|
|
||||||
|
|
@ -38,3 +49,8 @@ RETURNING *;
|
||||||
SELECT id, email, role, ativo, criado_em, atualizado_em
|
SELECT id, email, role, ativo, criado_em, atualizado_em
|
||||||
FROM usuarios
|
FROM usuarios
|
||||||
ORDER BY criado_em DESC;
|
ORDER BY criado_em DESC;
|
||||||
|
|
||||||
|
-- name: CreateCadastroCliente :one
|
||||||
|
INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
RETURNING *;
|
||||||
|
|
|
||||||
|
|
@ -301,3 +301,15 @@ BEGIN
|
||||||
ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor;
|
ON CONFLICT (tipo_evento_id, funcao_profissional_id) DO UPDATE SET valor = EXCLUDED.valor;
|
||||||
|
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- Cadastro Clientes (Representatives of Companies)
|
||||||
|
CREATE TABLE IF NOT EXISTS cadastro_clientes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||||
|
empresa_id UUID REFERENCES empresas(id) ON DELETE SET NULL,
|
||||||
|
nome VARCHAR(255), -- Name of the representative
|
||||||
|
telefone VARCHAR(20),
|
||||||
|
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
UNIQUE(usuario_id)
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
||||||
import { User, UserRole } from '../types';
|
import { User, UserRole } from '../types';
|
||||||
|
|
||||||
// Mock Users Database
|
// Mock Users Database
|
||||||
|
|
@ -38,14 +38,58 @@ interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (email: string, password?: string) => Promise<boolean>;
|
login: (email: string, password?: string) => Promise<boolean>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
|
register: (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => Promise<{ success: boolean; userId?: string; token?: string }>;
|
||||||
availableUsers: User[]; // Helper for the login screen demo
|
availableUsers: User[]; // Helper for the login screen demo
|
||||||
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [token, setToken] = useState<string | null>(localStorage.getItem("token"));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const restoreSession = async () => {
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/me`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
const backendUser = data.user;
|
||||||
|
const mappedUser: User = {
|
||||||
|
id: backendUser.id,
|
||||||
|
email: backendUser.email,
|
||||||
|
name: backendUser.email.split('@')[0],
|
||||||
|
role: backendUser.role as UserRole,
|
||||||
|
ativo: backendUser.ativo,
|
||||||
|
};
|
||||||
|
if (!backendUser.ativo) {
|
||||||
|
console.warn("User is not active, logging out.");
|
||||||
|
logout();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUser(mappedUser);
|
||||||
|
} else {
|
||||||
|
// Token invalid or expired
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Session restore error:", err);
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!user && token) {
|
||||||
|
restoreSession();
|
||||||
|
}
|
||||||
|
}, [token]); // removed 'user' from dependency to avoid loop if user is set, though !user check handles it. safer to just depend on token mount.
|
||||||
|
|
||||||
const getErrorMessage = (errorKey: string): string => {
|
const getErrorMessage = (errorKey: string): string => {
|
||||||
// Map backend error messages to Portuguese
|
// Map backend error messages to Portuguese
|
||||||
|
|
@ -93,9 +137,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Store token (optional, if you need it for other requests outside cookies)
|
|
||||||
localStorage.setItem('token', data.access_token);
|
|
||||||
|
|
||||||
const backendUser = data.user;
|
const backendUser = data.user;
|
||||||
|
|
||||||
// Enforce active check
|
// Enforce active check
|
||||||
|
|
@ -103,6 +144,10 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
throw new Error("Cadastro pendente de aprovação.");
|
throw new Error("Cadastro pendente de aprovação.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store token (optional, if you need it for other requests outside cookies)
|
||||||
|
localStorage.setItem('token', data.access_token);
|
||||||
|
setToken(data.access_token);
|
||||||
|
|
||||||
// Map backend user to frontend User type
|
// Map backend user to frontend User type
|
||||||
const mappedUser: User = {
|
const mappedUser: User = {
|
||||||
id: backendUser.id,
|
id: backendUser.id,
|
||||||
|
|
@ -121,17 +166,41 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
if (token) {
|
||||||
|
await fetch(`${import.meta.env.VITE_API_URL}/auth/logout`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}` // Though refresh token is in cookie
|
||||||
|
},
|
||||||
|
// body: JSON.stringify({ refresh_token: ... }) // If we had it in client state, but it is in cookie.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Logout failed", error);
|
||||||
|
} finally {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
// Force redirect or let app router handle it safely
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string }) => {
|
const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string }) => {
|
||||||
try {
|
try {
|
||||||
|
// Destructure to separate empresaId from the rest
|
||||||
|
const { empresaId, ...rest } = data;
|
||||||
|
const payload = {
|
||||||
|
...rest,
|
||||||
|
empresa_id: empresaId
|
||||||
|
};
|
||||||
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
|
const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -142,14 +211,15 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
|
|
||||||
const responseData = await response.json();
|
const responseData = await response.json();
|
||||||
|
|
||||||
if (responseData.access_token) {
|
|
||||||
localStorage.setItem('token', responseData.access_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// IF user is returned (auto-login), logic:
|
// IF user is returned (auto-login), logic:
|
||||||
// Only set user if they are ACTIVE (which they won't be for standard clients)
|
// Only set user/token if they are ACTIVE (which they won't be for standard clients/professionals)
|
||||||
// This allows the "Pending Approval" modal to show instead of auto-redirecting.
|
// This allows the "Pending Approval" modal to show instead of auto-redirecting.
|
||||||
if (responseData.user && responseData.user.ativo) {
|
if (responseData.user && responseData.user.ativo) {
|
||||||
|
if (responseData.access_token) {
|
||||||
|
localStorage.setItem('token', responseData.access_token);
|
||||||
|
setToken(responseData.access_token);
|
||||||
|
}
|
||||||
|
|
||||||
const backendUser = responseData.user;
|
const backendUser = responseData.user;
|
||||||
const mappedUser: User = {
|
const mappedUser: User = {
|
||||||
id: backendUser.id,
|
id: backendUser.id,
|
||||||
|
|
@ -160,6 +230,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
};
|
};
|
||||||
setUser(mappedUser);
|
setUser(mappedUser);
|
||||||
}
|
}
|
||||||
|
// If user is NOT active, we do NOT set the token/user state, preventing auto-login.
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -173,7 +244,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, login, logout, register, availableUsers: MOCK_USERS }}>
|
<AuthContext.Provider value={{ user, token, login, logout, register, availableUsers: MOCK_USERS }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
{ id: "1", name: "Dev Admin", email: "admin@photum.com", role: UserRole.SUPERADMIN },
|
{ id: "1", name: "Dev Admin", email: "admin@photum.com", role: UserRole.SUPERADMIN },
|
||||||
{ id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER },
|
{ id: "2", name: "PHOTUM CEO", email: "empresa@photum.com", role: UserRole.BUSINESS_OWNER },
|
||||||
{ id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER },
|
{ id: "3", name: "COLABORADOR PHOTUM", email: "foto@photum.com", role: UserRole.PHOTOGRAPHER },
|
||||||
|
{ id: "4", name: "CLIENTE TESTE", email: "cliente@photum.com", role: UserRole.EVENT_OWNER },
|
||||||
].map((user) => (
|
].map((user) => (
|
||||||
<button
|
<button
|
||||||
key={user.id}
|
key={user.id}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
senha: formData.password,
|
senha: formData.password,
|
||||||
telefone: formData.phone,
|
telefone: formData.phone,
|
||||||
role: "EVENT_OWNER", // Client Role
|
role: "EVENT_OWNER", // Client Role
|
||||||
|
empresaId: formData.empresaId,
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useData } from "../contexts/DataContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
|
import {
|
||||||
|
getPendingUsers,
|
||||||
|
approveUser as apiApproveUser,
|
||||||
|
rejectUser as apiRejectUser,
|
||||||
|
} from "../services/apiService";
|
||||||
import { UserApprovalStatus } from "../types";
|
import { UserApprovalStatus } from "../types";
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
|
@ -17,62 +22,111 @@ interface UserApprovalProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
const { pendingUsers, approveUser, rejectUser } = useData();
|
const { token } = useAuth();
|
||||||
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>(
|
const [statusFilter, setStatusFilter] = useState<"ALL" | UserApprovalStatus>(
|
||||||
"ALL"
|
"ALL"
|
||||||
);
|
);
|
||||||
const [activeTab, setActiveTab] = useState<"normal" | "professional">(
|
const [activeTab, setActiveTab] = useState<"cliente" | "profissional">(
|
||||||
"normal"
|
"cliente"
|
||||||
);
|
);
|
||||||
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
if (!token) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await getPendingUsers(token);
|
||||||
|
if (result.data) {
|
||||||
|
// Mapear dados do backend para o formato esperado pelo componente, se necessário
|
||||||
|
// Supondo que o backend retorna estrutura compatível ou fazemos o map aqui
|
||||||
|
const mappedUsers = result.data.map((u: any) => ({
|
||||||
|
...u,
|
||||||
|
approvalStatus: u.ativo
|
||||||
|
? UserApprovalStatus.APPROVED
|
||||||
|
: UserApprovalStatus.PENDING, // Simplificação, backend deve retornar status real se houver rejected
|
||||||
|
// Se o backend não retornar status explícito, assumimos pendente se !ativo
|
||||||
|
// Mas idealmente o backend retornaria um status enum
|
||||||
|
}));
|
||||||
|
setUsers(mappedUsers);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao buscar usuários:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
const handleApprove = async (userId: string) => {
|
const handleApprove = async (userId: string) => {
|
||||||
|
if (!token) return;
|
||||||
setIsProcessing(userId);
|
setIsProcessing(userId);
|
||||||
// Simular processamento
|
try {
|
||||||
setTimeout(() => {
|
await apiApproveUser(userId, token);
|
||||||
approveUser(userId);
|
// Atualizar lista após aprovação
|
||||||
|
await fetchUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao aprovar usuário:", error);
|
||||||
|
alert("Erro ao aprovar usuário");
|
||||||
|
} finally {
|
||||||
setIsProcessing(null);
|
setIsProcessing(null);
|
||||||
}, 800);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = async (userId: string) => {
|
const handleReject = async (userId: string) => {
|
||||||
|
if (!token) return;
|
||||||
setIsProcessing(userId);
|
setIsProcessing(userId);
|
||||||
// Simular processamento
|
try {
|
||||||
setTimeout(() => {
|
await apiRejectUser(userId, token);
|
||||||
rejectUser(userId);
|
await fetchUsers();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao rejeitar usuário:", error);
|
||||||
|
alert("Erro ao rejeitar usuário");
|
||||||
|
} finally {
|
||||||
setIsProcessing(null);
|
setIsProcessing(null);
|
||||||
}, 800);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Separar usuários normais e profissionais (profissionais têm role PHOTOGRAPHER)
|
// Separar usuários Clientes (EVENT_OWNER) e Profissionais (PHOTOGRAPHER)
|
||||||
const normalUsers = pendingUsers.filter(
|
// Backend roles: PHOTOGRAPHER, EVENT_OWNER, BUSINESS_OWNER, SUPERADMIN
|
||||||
(user) => user.role !== "PHOTOGRAPHER"
|
const clientUsers = users.filter(
|
||||||
|
(user) => user.role === "EVENT_OWNER"
|
||||||
);
|
);
|
||||||
const professionalUsers = pendingUsers.filter(
|
const professionalUsers = users.filter(
|
||||||
(user) => user.role === "PHOTOGRAPHER"
|
(user) => user.role === "PHOTOGRAPHER"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtrar usuários baseado na aba ativa
|
// Filtrar usuários baseado na aba ativa
|
||||||
const currentUsers = activeTab === "normal" ? normalUsers : professionalUsers;
|
const currentUsers = activeTab === "cliente" ? clientUsers : professionalUsers;
|
||||||
|
|
||||||
const filteredUsers = currentUsers.filter((user) => {
|
const filteredUsers = currentUsers.filter((user) => {
|
||||||
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());
|
||||||
(user.registeredInstitution
|
// Remover filtro por registeredInstitution se não vier do backend ainda
|
||||||
?.toLowerCase()
|
|
||||||
.includes(searchTerm.toLowerCase()) ??
|
|
||||||
false);
|
|
||||||
|
|
||||||
const matchesStatus =
|
// Por enquanto, como o backend retorna apenas pendentes na rota /pending (conforme nome da rota),
|
||||||
statusFilter === "ALL" || user.approvalStatus === statusFilter;
|
// o statusFilter pode ser redundante se a rota só traz pendentes.
|
||||||
|
// Mas se o backend trouxer todos, o filtro funciona.
|
||||||
|
// Se a rota for /users/pending, assumimos que todos são pendentes.
|
||||||
|
// VAMOS ASSUMIR QUE O BACKEND SÓ RETORNA PENDENTES POR ENQUANTO.
|
||||||
|
// Mas para manter a UI, vamos considerar todos como Pendentes se status não vier.
|
||||||
|
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStatusBadge = (status: UserApprovalStatus) => {
|
const getStatusBadge = (status: UserApprovalStatus) => {
|
||||||
switch (status) {
|
// Se status undefined, assume pendente para visualização
|
||||||
|
const s = status || UserApprovalStatus.PENDING;
|
||||||
|
switch (s) {
|
||||||
case UserApprovalStatus.PENDING:
|
case UserApprovalStatus.PENDING:
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
|
@ -94,19 +148,11 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
Rejeitado
|
Rejeitado
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pendingCount = currentUsers.filter(
|
|
||||||
(u) => u.approvalStatus === UserApprovalStatus.PENDING
|
|
||||||
).length;
|
|
||||||
const approvedCount = currentUsers.filter(
|
|
||||||
(u) => u.approvalStatus === UserApprovalStatus.APPROVED
|
|
||||||
).length;
|
|
||||||
const rejectedCount = currentUsers.filter(
|
|
||||||
(u) => u.approvalStatus === UserApprovalStatus.REJECTED
|
|
||||||
).length;
|
|
||||||
|
|
||||||
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">
|
||||||
|
|
@ -124,29 +170,26 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
<div className="mb-6 border-b border-gray-200">
|
<div className="mb-6 border-b border-gray-200">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("normal")}
|
onClick={() => setActiveTab("cliente")}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "cliente"
|
||||||
activeTab === "normal"
|
|
||||||
? "border-[#B9CF33] text-[#B9CF33]"
|
? "border-[#B9CF33] text-[#B9CF33]"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Users className="w-5 h-5" />
|
<Users className="w-5 h-5" />
|
||||||
Cadastros Normais
|
Cadastros Empresas
|
||||||
<span
|
<span
|
||||||
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${
|
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "cliente"
|
||||||
activeTab === "normal"
|
|
||||||
? "bg-[#B9CF33] text-white"
|
? "bg-[#B9CF33] text-white"
|
||||||
: "bg-gray-200 text-gray-600"
|
: "bg-gray-200 text-gray-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{normalUsers.length}
|
{clientUsers.length}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("professional")}
|
onClick={() => setActiveTab("profissional")}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-colors ${activeTab === "profissional"
|
||||||
activeTab === "professional"
|
|
||||||
? "border-[#B9CF33] text-[#B9CF33]"
|
? "border-[#B9CF33] text-[#B9CF33]"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -154,8 +197,7 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
<Briefcase className="w-5 h-5" />
|
<Briefcase className="w-5 h-5" />
|
||||||
Cadastros Profissionais
|
Cadastros Profissionais
|
||||||
<span
|
<span
|
||||||
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${
|
className={`ml-2 py-0.5 px-2.5 rounded-full text-xs ${activeTab === "profissional"
|
||||||
activeTab === "professional"
|
|
||||||
? "bg-[#B9CF33] text-white"
|
? "bg-[#B9CF33] text-white"
|
||||||
: "bg-gray-200 text-gray-600"
|
: "bg-gray-200 text-gray-600"
|
||||||
}`}
|
}`}
|
||||||
|
|
@ -166,51 +208,6 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Pendentes</p>
|
|
||||||
<p className="text-3xl font-bold text-yellow-600">
|
|
||||||
{pendingCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-yellow-100 rounded-full">
|
|
||||||
<Clock className="w-8 h-8 text-yellow-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Aprovados</p>
|
|
||||||
<p className="text-3xl font-bold text-green-600">
|
|
||||||
{approvedCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-green-100 rounded-full">
|
|
||||||
<CheckCircle className="w-8 h-8 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600">Rejeitados</p>
|
|
||||||
<p className="text-3xl font-bold text-red-600">
|
|
||||||
{rejectedCount}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-3 bg-red-100 rounded-full">
|
|
||||||
<XCircle className="w-8 h-8 text-red-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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">
|
||||||
|
|
@ -219,183 +216,40 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
<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"
|
||||||
placeholder="Buscar por nome, email ou universidade..."
|
placeholder="Buscar por nome ou email..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
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>
|
||||||
|
|
||||||
{/* Status Filter */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Filter className="w-5 h-5 text-gray-400" />
|
|
||||||
<select
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(e) => setStatusFilter(e.target.value as any)}
|
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
||||||
>
|
|
||||||
<option value="ALL">Todos os Status</option>
|
|
||||||
<option value={UserApprovalStatus.PENDING}>Pendentes</option>
|
|
||||||
<option value={UserApprovalStatus.APPROVED}>Aprovados</option>
|
|
||||||
<option value={UserApprovalStatus.REJECTED}>Rejeitados</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{activeTab === "normal" ? (
|
{isLoading ? (
|
||||||
// Tabela de Cadastros Normais
|
<div className="p-8 text-center text-gray-500">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
Carregando solicitações...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table key={activeTab} className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Nome
|
Nome
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
{activeTab === "cliente" && (
|
||||||
Email
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Telefone
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Empresa
|
Empresa
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Data de Cadastro
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Ações
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
|
||||||
{filteredUsers.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={7}
|
|
||||||
className="px-6 py-12 text-center text-gray-500"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<Clock className="w-12 h-12 text-gray-300 mb-3" />
|
|
||||||
<p className="text-lg font-medium">
|
|
||||||
Nenhum cadastro encontrado
|
|
||||||
</p>
|
|
||||||
<p className="text-sm">
|
|
||||||
Não há cadastros que correspondam aos filtros
|
|
||||||
selecionados.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredUsers.map((user) => (
|
|
||||||
<tr
|
|
||||||
key={user.id}
|
|
||||||
className="hover:bg-gray-50 transition-colors"
|
|
||||||
>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm font-medium text-gray-900">
|
|
||||||
{user.name}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{user.email}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{user.phone || "-"}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{user.registeredInstitution || (
|
|
||||||
<span className="text-gray-400 italic">
|
|
||||||
Não cadastrado
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{user.createdAt
|
|
||||||
? new Date(user.createdAt).toLocaleDateString(
|
|
||||||
"pt-BR"
|
|
||||||
)
|
|
||||||
: "-"}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
{getStatusBadge(user.approvalStatus!)}
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.PENDING && (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleApprove(user.id)}
|
|
||||||
isLoading={isProcessing === user.id}
|
|
||||||
disabled={isProcessing !== null}
|
|
||||||
className="bg-green-600 hover:bg-green-700 text-white"
|
|
||||||
>
|
|
||||||
<CheckCircle className="w-4 h-4 mr-1" />
|
|
||||||
Aprovar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleReject(user.id)}
|
|
||||||
isLoading={isProcessing === user.id}
|
|
||||||
disabled={isProcessing !== null}
|
|
||||||
className="bg-red-600 hover:bg-red-700 text-white"
|
|
||||||
>
|
|
||||||
<XCircle className="w-4 h-4 mr-1" />
|
|
||||||
Rejeitar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.APPROVED && (
|
|
||||||
<span className="text-green-600 text-xs">
|
|
||||||
Aprovado
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.REJECTED && (
|
|
||||||
<span className="text-red-600 text-xs">
|
|
||||||
Rejeitado
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) : (
|
|
||||||
// Tabela de Cadastros Profissionais
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Nome
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Email
|
Email
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Telefone
|
Telefone
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
||||||
Função Profissional
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Data de Cadastro
|
Data de Cadastro
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -411,32 +265,41 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
{filteredUsers.length === 0 ? (
|
{filteredUsers.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={7}
|
colSpan={activeTab === "cliente" ? 7 : 6}
|
||||||
className="px-6 py-12 text-center text-gray-500"
|
className="px-6 py-12 text-center text-gray-500"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
{activeTab === "cliente" ? (
|
||||||
|
<Users className="w-12 h-12 text-gray-300 mb-3" />
|
||||||
|
) : (
|
||||||
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
|
<Briefcase className="w-12 h-12 text-gray-300 mb-3" />
|
||||||
|
)}
|
||||||
<p className="text-lg font-medium">
|
<p className="text-lg font-medium">
|
||||||
Nenhum cadastro profissional encontrado
|
{activeTab === "cliente"
|
||||||
</p>
|
? "Nenhum cadastro de empresa encontrado"
|
||||||
<p className="text-sm">
|
: "Nenhum cadastro profissional encontrado"}
|
||||||
Não há cadastros profissionais que correspondam aos
|
|
||||||
filtros selecionados.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
filteredUsers.map((user) => (
|
filteredUsers.map((user, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={user.id}
|
key={`${user.id}-${index}`}
|
||||||
className="hover:bg-gray-50 transition-colors"
|
className="hover:bg-gray-50 transition-colors"
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
{user.name}
|
{user.name || user.email}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
{activeTab === "cliente" && (
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{user.company_name || "-"}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{user.email}
|
{user.email}
|
||||||
|
|
@ -449,28 +312,19 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
{(user as any).funcao || (
|
{user.created_at
|
||||||
<span className="text-gray-400 italic">
|
? new Date(user.created_at).toLocaleDateString(
|
||||||
Não cadastrado
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
{user.createdAt
|
|
||||||
? new Date(user.createdAt).toLocaleDateString(
|
|
||||||
"pt-BR"
|
"pt-BR"
|
||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
{getStatusBadge(user.approvalStatus!)}
|
{getStatusBadge(
|
||||||
|
user.approvalStatus || UserApprovalStatus.PENDING
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.PENDING && (
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -493,26 +347,14 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
|
||||||
Rejeitar
|
Rejeitar
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.APPROVED && (
|
|
||||||
<span className="text-green-600 text-xs">
|
|
||||||
Aprovado
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{user.approvalStatus ===
|
|
||||||
UserApprovalStatus.REJECTED && (
|
|
||||||
<span className="text-red-600 text-xs">
|
|
||||||
Rejeitado
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</div >
|
</div >
|
||||||
</div >
|
</div >
|
||||||
</div >
|
</div >
|
||||||
|
|
|
||||||
|
|
@ -262,3 +262,37 @@ export async function approveUser(userId: string, token: string): Promise<ApiRes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rejeita um usuário
|
||||||
|
*/
|
||||||
|
export async function rejectUser(userId: string, token: string): Promise<ApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/admin/users/${userId}/reject`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
},
|
||||||
|
// body: JSON.stringify({ reason }) // Future improvement
|
||||||
|
});
|
||||||
|
|
||||||
|
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 rejecting user:", error);
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
isBackendDown: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue