From 5312945f7c28c09d9c6a3dba70d25f8c2e359678 Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 29 Dec 2025 16:51:55 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20Adicionado=20gerenciamento=20de=20usu?= =?UTF-8?q?=C3=A1rios=20administradores,=20funcionalidade=20de=20c=C3=B3di?= =?UTF-8?q?go=20de=20acesso=20e=20documenta=C3=A7=C3=A3o=20da=20API.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/api/main.go | 9 + backend/docs/docs.go | 97 +++++++ backend/docs/swagger.json | 97 +++++++ backend/docs/swagger.yaml | 63 +++++ backend/internal/codigos/handler.go | 90 +++++++ backend/internal/codigos/service.go | 79 ++++++ .../db/generated/codigos_acesso.sql.go | 127 +++++++++ backend/internal/db/generated/models.go | 11 + .../internal/db/queries/codigos_acesso.sql | 24 ++ backend/internal/db/schema.sql | 12 + backend/temp_schema_append.sql | 12 + frontend/App.tsx | 4 +- frontend/pages/AccessCodes.tsx | 246 ++++++++++++++++++ frontend/services/apiService.ts | 57 +++- 14 files changed, 923 insertions(+), 5 deletions(-) create mode 100644 backend/internal/codigos/handler.go create mode 100644 backend/internal/codigos/service.go create mode 100644 backend/internal/db/generated/codigos_acesso.sql.go create mode 100644 backend/internal/db/queries/codigos_acesso.sql create mode 100644 backend/temp_schema_append.sql create mode 100644 frontend/pages/AccessCodes.tsx diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index 40d0c63..3e500b8 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -10,6 +10,7 @@ import ( "photum-backend/internal/auth" "photum-backend/internal/availability" "photum-backend/internal/cadastro_fot" + "photum-backend/internal/codigos" "photum-backend/internal/config" "photum-backend/internal/cursos" "photum-backend/internal/db" @@ -96,6 +97,7 @@ func main() { availabilityHandler := availability.NewHandler(availabilityService) escalasHandler := escalas.NewHandler(escalas.NewService(queries)) logisticaHandler := logistica.NewHandler(logistica.NewService(queries)) + codigosHandler := codigos.NewHandler(codigos.NewService(queries)) r := gin.Default() @@ -230,6 +232,13 @@ func main() { logisticaGroup.GET("/carros/:id/passageiros", logisticaHandler.ListPassengers) } + codigosGroup := api.Group("/codigos-acesso") + { + codigosGroup.POST("", codigosHandler.Create) + codigosGroup.GET("", codigosHandler.List) + codigosGroup.DELETE("/:id", codigosHandler.Delete) + } + admin := api.Group("/admin") { admin.GET("/users", authHandler.ListUsers) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index b70af06..0b5b7ef 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1056,6 +1056,89 @@ const docTemplate = `{ } } }, + "/api/codigos-acesso": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "codigos" + ], + "summary": "List Access Codes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "codigos" + ], + "summary": "Create Access Code", + "parameters": [ + { + "description": "Req", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codigos.CreateCodigoInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/codigos-acesso/{id}": { + "delete": { + "tags": [ + "codigos" + ], + "summary": "Delete Access Code", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/cursos": { "get": { "security": [ @@ -3046,6 +3129,20 @@ const docTemplate = `{ } } }, + "codigos.CreateCodigoInput": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "descricao": { + "type": "string" + }, + "validade_dias": { + "type": "integer" + } + } + }, "cursos.CreateCursoRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 7772867..2fe7702 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1050,6 +1050,89 @@ } } }, + "/api/codigos-acesso": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "codigos" + ], + "summary": "List Access Codes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "codigos" + ], + "summary": "Create Access Code", + "parameters": [ + { + "description": "Req", + "name": "req", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codigos.CreateCodigoInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/codigos-acesso/{id}": { + "delete": { + "tags": [ + "codigos" + ], + "summary": "Delete Access Code", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/cursos": { "get": { "security": [ @@ -3040,6 +3123,20 @@ } } }, + "codigos.CreateCodigoInput": { + "type": "object", + "properties": { + "codigo": { + "type": "string" + }, + "descricao": { + "type": "string" + }, + "validade_dias": { + "type": "integer" + } + } + }, "cursos.CreateCursoRequest": { "type": "object", "required": [ diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 59d6cfb..096d168 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -210,6 +210,15 @@ definitions: pre_venda: type: boolean type: object + codigos.CreateCodigoInput: + properties: + codigo: + type: string + descricao: + type: string + validade_dias: + type: integer + type: object cursos.CreateCursoRequest: properties: nome: @@ -1176,6 +1185,60 @@ paths: summary: Update FOT record tags: - cadastro_fot + /api/codigos-acesso: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + additionalProperties: true + type: object + type: array + summary: List Access Codes + tags: + - codigos + post: + consumes: + - application/json + parameters: + - description: Req + in: body + name: req + required: true + schema: + $ref: '#/definitions/codigos.CreateCodigoInput' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + summary: Create Access Code + tags: + - codigos + /api/codigos-acesso/{id}: + delete: + parameters: + - description: ID + in: path + name: id + required: true + type: string + responses: + "200": + description: OK + schema: + additionalProperties: + type: string + type: object + summary: Delete Access Code + tags: + - codigos /api/cursos: get: consumes: diff --git a/backend/internal/codigos/handler.go b/backend/internal/codigos/handler.go new file mode 100644 index 0000000..c59fd5c --- /dev/null +++ b/backend/internal/codigos/handler.go @@ -0,0 +1,90 @@ +package codigos + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +// Create godoc +// @Summary Create Access Code +// @Tags codigos +// @Accept json +// @Produce json +// @Param req body CreateCodigoInput true "Req" +// @Success 201 {object} map[string]interface{} +// @Router /api/codigos-acesso [post] +func (h *Handler) Create(c *gin.Context) { + var req CreateCodigoInput + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + code, err := h.service.Create(c.Request.Context(), req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"id": uuid.UUID(code.ID.Bytes).String(), "codigo": code.Codigo}) +} + +// List godoc +// @Summary List Access Codes +// @Tags codigos +// @Produce json +// @Success 200 {array} map[string]interface{} +// @Router /api/codigos-acesso [get] +func (h *Handler) List(c *gin.Context) { + codes, err := h.service.List(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + resp := make([]map[string]interface{}, len(codes)) + for i, v := range codes { + resp[i] = map[string]interface{}{ + "id": uuid.UUID(v.ID.Bytes).String(), + "codigo": v.Codigo, + "descricao": v.Descricao.String, + "validade_dias": v.ValidadeDias, + "criado_em": v.CriadoEm.Time, + "expira_em": v.ExpiraEm.Time, + "ativo": v.Ativo, + "usos": v.Usos, + } + } + c.JSON(http.StatusOK, resp) +} + +// Delete godoc +// @Summary Delete Access Code +// @Tags codigos +// @Param id path string true "ID" +// @Success 200 {object} map[string]string +// @Router /api/codigos-acesso/{id} [delete] +func (h *Handler) Delete(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "id required"}) + return + } + + err := h.service.Delete(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "deleted"}) +} diff --git a/backend/internal/codigos/service.go b/backend/internal/codigos/service.go new file mode 100644 index 0000000..8a2303d --- /dev/null +++ b/backend/internal/codigos/service.go @@ -0,0 +1,79 @@ +package codigos + +import ( + "context" + "time" + + "photum-backend/internal/db/generated" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +type Service struct { + q *generated.Queries +} + +func NewService(q *generated.Queries) *Service { + return &Service{ + q: q, + } +} + +type CreateCodigoInput struct { + Codigo string `json:"codigo"` + Descricao string `json:"descricao"` + ValidadeDias int32 `json:"validade_dias"` +} + +func (s *Service) Create(ctx context.Context, input CreateCodigoInput) (generated.CodigosAcesso, error) { + days := input.ValidadeDias + + var expiraEm time.Time + if days == -1 { + // Infinite: 100 years + expiraEm = time.Now().AddDate(100, 0, 0) + } else { + if days <= 0 { + days = 30 + } + expiraEm = time.Now().Add(time.Duration(days) * 24 * time.Hour) + } + + return s.q.CreateCodigoAcesso(ctx, generated.CreateCodigoAcessoParams{ + Codigo: input.Codigo, + Descricao: pgtype.Text{String: input.Descricao, Valid: input.Descricao != ""}, + ValidadeDias: days, + ExpiraEm: pgtype.Timestamptz{Time: expiraEm, Valid: true}, + Ativo: true, + }) +} + +func (s *Service) List(ctx context.Context) ([]generated.CodigosAcesso, error) { + return s.q.ListCodigosAcesso(ctx) +} + +func (s *Service) Delete(ctx context.Context, id string) error { + uid, err := uuid.Parse(id) + if err != nil { + return err + } + + // Convert uuid.UUID to pgtype.UUID + var pgUUID pgtype.UUID + pgUUID.Bytes = uid + pgUUID.Valid = true + + return s.q.DeleteCodigoAcesso(ctx, pgUUID) +} + +func (s *Service) GetByCode(ctx context.Context, code string) (generated.CodigosAcesso, error) { + return s.q.GetCodigoAcesso(ctx, code) +} + +func (s *Service) IncrementUse(ctx context.Context, id uuid.UUID) error { + var pgUUID pgtype.UUID + pgUUID.Bytes = id + pgUUID.Valid = true + return s.q.IncrementCodigoAcessoUso(ctx, pgUUID) +} diff --git a/backend/internal/db/generated/codigos_acesso.sql.go b/backend/internal/db/generated/codigos_acesso.sql.go new file mode 100644 index 0000000..e893de4 --- /dev/null +++ b/backend/internal/db/generated/codigos_acesso.sql.go @@ -0,0 +1,127 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: codigos_acesso.sql + +package generated + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createCodigoAcesso = `-- name: CreateCodigoAcesso :one +INSERT INTO codigos_acesso ( + codigo, descricao, validade_dias, expira_em, ativo +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING id, codigo, descricao, validade_dias, criado_em, expira_em, ativo, usos +` + +type CreateCodigoAcessoParams struct { + Codigo string `json:"codigo"` + Descricao pgtype.Text `json:"descricao"` + ValidadeDias int32 `json:"validade_dias"` + ExpiraEm pgtype.Timestamptz `json:"expira_em"` + Ativo bool `json:"ativo"` +} + +func (q *Queries) CreateCodigoAcesso(ctx context.Context, arg CreateCodigoAcessoParams) (CodigosAcesso, error) { + row := q.db.QueryRow(ctx, createCodigoAcesso, + arg.Codigo, + arg.Descricao, + arg.ValidadeDias, + arg.ExpiraEm, + arg.Ativo, + ) + var i CodigosAcesso + err := row.Scan( + &i.ID, + &i.Codigo, + &i.Descricao, + &i.ValidadeDias, + &i.CriadoEm, + &i.ExpiraEm, + &i.Ativo, + &i.Usos, + ) + return i, err +} + +const deleteCodigoAcesso = `-- name: DeleteCodigoAcesso :exec +DELETE FROM codigos_acesso +WHERE id = $1 +` + +func (q *Queries) DeleteCodigoAcesso(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, deleteCodigoAcesso, id) + return err +} + +const getCodigoAcesso = `-- name: GetCodigoAcesso :one +SELECT id, codigo, descricao, validade_dias, criado_em, expira_em, ativo, usos FROM codigos_acesso +WHERE codigo = $1 LIMIT 1 +` + +func (q *Queries) GetCodigoAcesso(ctx context.Context, codigo string) (CodigosAcesso, error) { + row := q.db.QueryRow(ctx, getCodigoAcesso, codigo) + var i CodigosAcesso + err := row.Scan( + &i.ID, + &i.Codigo, + &i.Descricao, + &i.ValidadeDias, + &i.CriadoEm, + &i.ExpiraEm, + &i.Ativo, + &i.Usos, + ) + return i, err +} + +const incrementCodigoAcessoUso = `-- name: IncrementCodigoAcessoUso :exec +UPDATE codigos_acesso +SET usos = usos + 1 +WHERE id = $1 +` + +func (q *Queries) IncrementCodigoAcessoUso(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, incrementCodigoAcessoUso, id) + return err +} + +const listCodigosAcesso = `-- name: ListCodigosAcesso :many +SELECT id, codigo, descricao, validade_dias, criado_em, expira_em, ativo, usos FROM codigos_acesso +ORDER BY criado_em DESC +` + +func (q *Queries) ListCodigosAcesso(ctx context.Context) ([]CodigosAcesso, error) { + rows, err := q.db.Query(ctx, listCodigosAcesso) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CodigosAcesso + for rows.Next() { + var i CodigosAcesso + if err := rows.Scan( + &i.ID, + &i.Codigo, + &i.Descricao, + &i.ValidadeDias, + &i.CriadoEm, + &i.ExpiraEm, + &i.Ativo, + &i.Usos, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index 8566f04..7964435 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -124,6 +124,17 @@ type CadastroProfissionai struct { AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` } +type CodigosAcesso struct { + ID pgtype.UUID `json:"id"` + Codigo string `json:"codigo"` + Descricao pgtype.Text `json:"descricao"` + ValidadeDias int32 `json:"validade_dias"` + CriadoEm pgtype.Timestamptz `json:"criado_em"` + ExpiraEm pgtype.Timestamptz `json:"expira_em"` + Ativo bool `json:"ativo"` + Usos int32 `json:"usos"` +} + type Curso struct { ID pgtype.UUID `json:"id"` Nome string `json:"nome"` diff --git a/backend/internal/db/queries/codigos_acesso.sql b/backend/internal/db/queries/codigos_acesso.sql new file mode 100644 index 0000000..b84808e --- /dev/null +++ b/backend/internal/db/queries/codigos_acesso.sql @@ -0,0 +1,24 @@ +-- name: CreateCodigoAcesso :one +INSERT INTO codigos_acesso ( + codigo, descricao, validade_dias, expira_em, ativo +) VALUES ( + $1, $2, $3, $4, $5 +) +RETURNING *; + +-- name: ListCodigosAcesso :many +SELECT * FROM codigos_acesso +ORDER BY criado_em DESC; + +-- name: DeleteCodigoAcesso :exec +DELETE FROM codigos_acesso +WHERE id = $1; + +-- name: GetCodigoAcesso :one +SELECT * FROM codigos_acesso +WHERE codigo = $1 LIMIT 1; + +-- name: IncrementCodigoAcessoUso :exec +UPDATE codigos_acesso +SET usos = usos + 1 +WHERE id = $1; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index a82fa15..2a3f3e7 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -421,3 +421,15 @@ CREATE TABLE IF NOT EXISTS logistica_passageiros ( criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(carro_id, profissional_id) ); + +-- Codigos de Acesso Table +CREATE TABLE IF NOT EXISTS codigos_acesso ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + codigo VARCHAR(50) UNIQUE NOT NULL, + descricao VARCHAR(255), + validade_dias INT NOT NULL DEFAULT 30, + criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expira_em TIMESTAMPTZ NOT NULL, + ativo BOOLEAN NOT NULL DEFAULT TRUE, + usos INT NOT NULL DEFAULT 0 +); diff --git a/backend/temp_schema_append.sql b/backend/temp_schema_append.sql new file mode 100644 index 0000000..0a8eb34 --- /dev/null +++ b/backend/temp_schema_append.sql @@ -0,0 +1,12 @@ + +-- Codigos de Acesso Table +CREATE TABLE IF NOT EXISTS codigos_acesso ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + codigo VARCHAR(50) UNIQUE NOT NULL, + descricao VARCHAR(255), + validade_dias INT NOT NULL DEFAULT 30, + criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expira_em TIMESTAMPTZ NOT NULL, + ativo BOOLEAN NOT NULL DEFAULT TRUE, + usos INT NOT NULL DEFAULT 0 +); diff --git a/frontend/App.tsx b/frontend/App.tsx index a46d7cd..e8563b0 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -21,7 +21,7 @@ import { SettingsPage } from "./pages/Settings"; import { CourseManagement } from "./pages/CourseManagement"; import { InspirationPage } from "./pages/Inspiration"; import { UserApproval } from "./pages/UserApproval"; -import { AccessCodeManagement } from "./pages/AccessCodeManagement"; +import { AccessCodes } from "./pages/AccessCodes"; import { PrivacyPolicy } from "./pages/PrivacyPolicy"; import { TermsOfUse } from "./pages/TermsOfUse"; import { LGPD } from "./pages/LGPD"; @@ -540,7 +540,7 @@ const AppContent: React.FC = () => { allowedRoles={[UserRole.SUPERADMIN, UserRole.BUSINESS_OWNER]} > - { }} /> + } diff --git a/frontend/pages/AccessCodes.tsx b/frontend/pages/AccessCodes.tsx new file mode 100644 index 0000000..1a210bf --- /dev/null +++ b/frontend/pages/AccessCodes.tsx @@ -0,0 +1,246 @@ +import React, { useState, useEffect } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { createAccessCode, listAccessCodes, deleteAccessCode } from '../services/apiService'; +import { Plus, Trash2, Copy, Check } from 'lucide-react'; +import { UserRole } from '../types'; + +interface AccessCode { + id: string; + codigo: string; + descricao: string; + validade_dias: number; + criado_em: string; + expira_em: string; + ativo: boolean; + usos: number; +} + +export const AccessCodes: React.FC = () => { + const { token, user } = useAuth(); + const [codes, setCodes] = useState([]); + const [loading, setLoading] = useState(false); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + // Form State + const [newCode, setNewCode] = useState(''); + const [days, setDays] = useState(30); + const [customDays, setCustomDays] = useState(''); + const [copiedId, setCopiedId] = useState(null); + + useEffect(() => { + if (token) fetchCodes(); + }, [token]); + + const fetchCodes = async () => { + setLoading(true); + const res = await listAccessCodes(token!); + if (res.data) { + setCodes(res.data); + } + setLoading(false); + }; + + const handleCreate = async (e: React.FormEvent) => { + e.preventDefault(); + const codeToCreate = newCode.trim() || generateRandomCode(); + + const res = await createAccessCode(token!, { + codigo: codeToCreate, + validade_dias: days, + descricao: 'Gerado via Painel' + }); + + if (res.error) { + alert('Erro ao criar: ' + res.error); + } else { + setIsCreateModalOpen(false); + setNewCode(''); + fetchCodes(); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm('Excluir este código? Ele não poderá mais ser usado.')) return; + + const res = await deleteAccessCode(token!, id); + if (!res.error) { + fetchCodes(); + } else { + alert('Erro ao excluir'); + } + }; + + const generateRandomCode = () => { + const year = new Date().getFullYear(); + const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0'); + return `PHOTUM${year}-${random}`; + }; + + const copyToClipboard = (text: string, id: string) => { + navigator.clipboard.writeText(text); + setCopiedId(id); + setTimeout(() => setCopiedId(null), 2000); + }; + + const formatDate = (dateStr: string) => { + return new Date(dateStr).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); + }; + + if (!user || (user.role !== UserRole.SUPERADMIN && user.role !== UserRole.BUSINESS_OWNER)) { + return
Acesso restrito.
; + } + + return ( +
+
+

Gerenciar Códigos de Acesso

+

Crie e gerencie códigos de acesso temporários para novos cadastros

+
+ + + +
+
+ + + + + + + + + + + + + {loading ? ( + + ) : codes.length === 0 ? ( + + ) : ( + codes.map(code => ( + + + + + + + + + )) + )} + +
StatusCódigoValidade (Dias)Data de CriaçãoData de ExpiraçãoAções
Carregando...
Nenhum código gerado.
+ + {code.validade_dias === -1 ? 'Infinito' : `${code.validade_dias} dias`} + + +
+ {code.codigo} + +
+
{code.validade_dias === -1 ? 'Nunca expira' : `${code.validade_dias} dias`}{formatDate(code.criado_em)}{code.validade_dias === -1 ? '-' : formatDate(code.expira_em)} + +
+
+
+ + {/* Modal Create */} + {isCreateModalOpen && ( +
+
+

Novo Código de Acesso

+
+
+ + setNewCode(e.target.value.toUpperCase())} + placeholder={generateRandomCode()} + className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-[#492E61] focus:border-transparent outline-none" + /> +

Deixe em branco para gerar automaticamente.

+
+
+ +
+
+ {[7, 15, 30, 60, 90].map(d => ( + + ))} + +
+ + {days !== -1 && ( +
+ Outro: + { + const val = parseInt(e.target.value); + setCustomDays(e.target.value); + if (!isNaN(val) && val > 0) setDays(val); + }} + className="w-24 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:ring-2 focus:ring-[#492E61] focus:border-transparent outline-none" + placeholder="Dias" + /> +
+ )} +
+
+
+ + +
+
+
+
+ )} +
+ ); +}; diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 0521082..034efde 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -802,8 +802,7 @@ export async function getUploadURL(filename: string, contentType: string): Promi }); if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); @@ -813,7 +812,7 @@ export async function getUploadURL(filename: string, contentType: string): Promi isBackendDown: false, }; } catch (error) { - console.error("Error fetching upload URL:", error); + console.error("Error getting upload URL:", error); return { data: null, error: error instanceof Error ? error.message : "Erro desconhecido", @@ -822,6 +821,58 @@ export async function getUploadURL(filename: string, contentType: string): Promi } } +// Access Codes +export async function createAccessCode(token: string, data: { codigo: string; descricao?: string; validade_dias: number }) { + return mutationFetch(`${API_BASE_URL}/api/codigos-acesso`, "POST", data, token); +} + +export async function listAccessCodes(token: string) { + return fetchFromBackendAuthenticated(`${API_BASE_URL}/api/codigos-acesso`, token); +} + +export async function deleteAccessCode(token: string, id: string) { + return mutationFetch(`${API_BASE_URL}/api/codigos-acesso/${id}`, "DELETE", {}, token); +} + +// Helpers for unified fetch +async function fetchFromBackendAuthenticated(url: string, token: string) { + try { + const res = await fetch(url, { + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + } + }); + if (!res.ok) throw new Error(res.statusText); + const data = await res.json(); + return { data, error: null }; + } catch (e: any) { + return { data: null, error: e.message }; + } +} + +async function mutationFetch(url: string, method: string, body: any, token: string) { + try { + const res = await fetch(url, { + method, + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || res.statusText); + } + const data = await res.json(); + return { data, error: null }; + } catch (e: any) { + return { data: null, error: e.message }; + } +} + + /** * Realiza o upload do arquivo para a URL pré-assinada */