From 0a0c344022971cb5abcde20f0149b160aa3bdcde Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Wed, 21 Jan 2026 17:20:06 -0300 Subject: [PATCH] =?UTF-8?q?feat(geral):=20implementa=20fluxo=20de=20aprova?= =?UTF-8?q?=C3=A7=C3=A3o,=20api=20de=20endere=C3=A7os=20e=20acesso=20maste?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Implementa API de Endereços (`POST /enderecos`) e migration da tabela `addresses`. - Adiciona bloqueio de login para usuários de empresas não verificadas (status `pending`). - Criação automática do usuário Master (`seedAdmin`) com empresa verificada. - Adiciona aliases de rota em PT-BR (`/api/v1/empresas` GET/PATCH, `/api/v1/usuarios` PATCH) para compatibilidade com o frontend. - Atualiza DTOs para suportar campos em português no registro de empresas e atualização de usuários. - Endpoint `/auth/me` agora retorna `company_name` e flag `superadmin`. - Ajusta filtro de repositório para listar empresas por status de verificação. Frontend: - Nova página `/usuarios-pendentes` com layout padrão e funcionalidade de aprovação. - Atualiza [Header](cci:1://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/components/Header.tsx:29:0-337:2) para exibir o nome da empresa do usuário logado. - Serviço `empresaApiService`: correções de mapeamento (`corporate_name` -> `razao_social`) e novos métodos. - Tipagem atualizada para incluir campos de empresa no [UserData](cci:2://file:///c:/Projetos/saveinmed/saveinmed-frontend/src/types/auth.ts:15:0-30:1). Fixes: - Correção de erro 405 (Method Not Allowed) nas rotas de atualização. - Correção de erro 404 na listagem de pendentes. - Resolução de campos vazios na listagem de empresas. --- backend-old/.env | 28 +- backend-old/internal/domain/models.go | 29 +- .../internal/http/handler/address_handler.go | 48 ++ .../internal/http/handler/company_handler.go | 24 +- backend-old/internal/http/handler/dto.go | 68 +- .../http/handler/financial_handler.go | 16 +- backend-old/internal/http/handler/handler.go | 64 +- .../internal/http/handler/handler_test.go | 5 + .../internal/http/handler/user_handler.go | 8 + backend-old/internal/http/middleware/cors.go | 3 +- .../0005_tenants_operating_hours.sql | 5 +- .../0006_product_catalog_fields.sql | 7 +- ...fied.sql => 0010_users_email_verified.sql} | 0 .../0011_create_addresses_table.sql | 16 + .../internal/repository/postgres/postgres.go | 12 + backend-old/internal/server/server.go | 17 +- backend-old/internal/usecase/usecase.go | 19 + backend-old/internal/usecase/usecase_test.go | 8 + backend-old/login.json | 1 + .../src/app/api/cep/[cep]/route.ts | 3 +- .../src/app/completar-registro/page.tsx | 1 + saveinmed-frontend/src/app/login/page.tsx | 28 +- .../src/app/usuarios-pendentes/page.tsx | 742 ++++-------------- saveinmed-frontend/src/components/Header.tsx | 7 +- .../src/services/empresaApiService.ts | 46 ++ saveinmed-frontend/src/types/auth.ts | 1 + 26 files changed, 533 insertions(+), 673 deletions(-) create mode 100644 backend-old/internal/http/handler/address_handler.go rename backend-old/internal/repository/postgres/migrations/{0004_users_email_verified.sql => 0010_users_email_verified.sql} (100%) create mode 100644 backend-old/internal/repository/postgres/migrations/0011_create_addresses_table.sql create mode 100644 backend-old/login.json diff --git a/backend-old/.env b/backend-old/.env index 523883b..da3eeac 100644 --- a/backend-old/.env +++ b/backend-old/.env @@ -7,20 +7,34 @@ APP_NAME=saveinmed-performance-core BACKEND_PORT=8214 # Database Configuration -DATABASE_URL=postgres://yuki:xl1zfmr6e9bb@db-60059.dc-sp-1.absamcloud.com:26868/sim_dev?sslmode=disable +DATABASE_URL=postgres://postgres:123@localhost:55432/saveinmed?sslmode=disable +ADMIN_NAME=Admin Master +ADMIN_USERNAME=admin +ADMIN_EMAIL=andre.fr93@gmail.com +ADMIN_PASSWORD=teste1234 # JWT Authentication JWT_SECRET=your-secret-key-here JWT_EXPIRES_IN=24h +PASSWORD_PEPPER=your-password-pepper # MercadoPago Payment Gateway MERCADOPAGO_BASE_URL=https://api.mercadopago.com MARKETPLACE_COMMISSION=2.5 -# CORS Configuration (comma-separated list of allowed origins, use * for all) -CORS_ORIGINS=* +# CORS Configuration +# Comma-separated list of allowed origins, use * for all +# Examples: +# CORS_ORIGINS=* +# CORS_ORIGINS=https://example.com +# CORS_ORIGINS=https://app.saveinmed.com,https://admin.saveinmed.com,http://localhost:3000 +CORS_ORIGINS=http://localhost:3000 -ADMIN_NAME=Administrator -ADMIN_USERNAME=admin -ADMIN_EMAIL=admin@saveinmed.com -ADMIN_PASSWORD=admin123 \ No newline at end of file +# Swagger Configuration +# Host without scheme (ex: localhost:8214 or api.saveinmed.com) +BACKEND_HOST=localhost:8214 +# Comma-separated list of schemes shown in Swagger UI selector +SWAGGER_SCHEMES=http,https + +# Testing (Optional) +# SKIP_DB_TEST=1 diff --git a/backend-old/internal/domain/models.go b/backend-old/internal/domain/models.go index 28eaf5c..cdabc0e 100644 --- a/backend-old/internal/domain/models.go +++ b/backend-old/internal/domain/models.go @@ -139,12 +139,13 @@ type ProductPage struct { // CompanyFilter captures company/tenant listing constraints. type CompanyFilter struct { - Category string - Search string - City string - State string - Limit int - Offset int + Category string + Search string + City string + State string + IsVerified *bool + Limit int + Offset int } // TenantFilter is an alias for CompanyFilter. @@ -321,6 +322,22 @@ type PaymentGatewayConfig struct { UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } +// Address represents a physical location for users or companies. +type Address struct { + ID uuid.UUID `db:"id" json:"id"` + EntityID uuid.UUID `db:"entity_id" json:"entity_id"` + Title string `db:"title" json:"titulo"` + ZipCode string `db:"zip_code" json:"cep"` + Street string `db:"street" json:"logradouro"` + Number string `db:"number" json:"numero"` + Complement string `db:"complement" json:"complemento"` + District string `db:"district" json:"bairro"` + City string `db:"city" json:"cidade"` + State string `db:"state" json:"uf"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + // ShippingAddress captures delivery details at order time. type ShippingAddress struct { RecipientName string `json:"recipient_name" db:"shipping_recipient_name"` diff --git a/backend-old/internal/http/handler/address_handler.go b/backend-old/internal/http/handler/address_handler.go new file mode 100644 index 0000000..920756d --- /dev/null +++ b/backend-old/internal/http/handler/address_handler.go @@ -0,0 +1,48 @@ +package handler + +import ( + "log" + "net/http" + "time" + + "github.com/saveinmed/backend-go/internal/domain" +) + +func (h *Handler) CreateAddress(w http.ResponseWriter, r *http.Request) { + reqUser, err := getRequester(r) + if err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + var payload createAddressRequest + if err := decodeJSON(r.Context(), r, &payload); err != nil { + writeError(w, http.StatusBadRequest, err) + return + } + + // Use user ID as the entity ID + entityID := reqUser.ID + + addr := domain.Address{ + EntityID: entityID, + Title: payload.Title, + ZipCode: payload.ZipCode, + Street: payload.Street, + Number: payload.Number, + Complement: payload.Complement, + District: payload.District, + City: payload.City, + State: payload.State, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := h.svc.CreateAddress(r.Context(), &addr); err != nil { + log.Printf("Failed to create address: %v", err) + writeError(w, http.StatusInternalServerError, err) + return + } + + writeJSON(w, http.StatusCreated, addr) +} diff --git a/backend-old/internal/http/handler/company_handler.go b/backend-old/internal/http/handler/company_handler.go index d0ea92c..90732a7 100644 --- a/backend-old/internal/http/handler/company_handler.go +++ b/backend-old/internal/http/handler/company_handler.go @@ -3,6 +3,7 @@ package handler import ( "errors" "net/http" + "strconv" "strings" "github.com/saveinmed/backend-go/internal/domain" @@ -25,15 +26,30 @@ func (h *Handler) CreateCompany(w http.ResponseWriter, r *http.Request) { return } + // Map Portuguese fields if English ones are empty + if req.CorporateName == "" { + req.CorporateName = req.RazaoSocial + } + if req.Category == "" { + // Default category if not provided or map from Activity Code? + // For now, use description or default + if req.DescricaoAtividade != "" { + req.Category = req.DescricaoAtividade + } else { + req.Category = "farmacia" // Default + } + } + company := &domain.Company{ Category: req.Category, CNPJ: req.CNPJ, CorporateName: req.CorporateName, - LicenseNumber: req.LicenseNumber, + LicenseNumber: req.LicenseNumber, // Frontend might not send this yet? Latitude: req.Latitude, Longitude: req.Longitude, City: req.City, State: req.State, + Phone: req.Telefone, } if err := h.svc.RegisterCompany(r.Context(), company); err != nil { @@ -59,6 +75,12 @@ func (h *Handler) ListCompanies(w http.ResponseWriter, r *http.Request) { State: r.URL.Query().Get("state"), } + if v := r.URL.Query().Get("is_verified"); v != "" { + if b, err := strconv.ParseBool(v); err == nil { + filter.IsVerified = &b + } + } + result, err := h.svc.ListCompanies(r.Context(), filter, page, pageSize) if err != nil { writeError(w, http.StatusInternalServerError, err) diff --git a/backend-old/internal/http/handler/dto.go b/backend-old/internal/http/handler/dto.go index 9d34c90..b753aba 100644 --- a/backend-old/internal/http/handler/dto.go +++ b/backend-old/internal/http/handler/dto.go @@ -66,7 +66,7 @@ type verifyEmailRequest struct { } type authResponse struct { - Token string `json:"token"` + Token string `json:"access_token"` ExpiresAt time.Time `json:"expires_at"` } @@ -98,15 +98,30 @@ type createReviewRequest struct { } type updateUserRequest struct { - CompanyID *uuid.UUID `json:"company_id,omitempty"` - Role *string `json:"role,omitempty"` - Name *string `json:"name,omitempty"` - Username *string `json:"username,omitempty"` - Email *string `json:"email,omitempty"` - Password *string `json:"password,omitempty"` + CompanyID *uuid.UUID `json:"company_id,omitempty"` + Role *string `json:"role,omitempty"` + Name *string `json:"name,omitempty"` + Username *string `json:"username,omitempty"` + Email *string `json:"email,omitempty"` + Password *string `json:"password,omitempty"` + EmpresasDados []string `json:"empresasDados"` // Frontend sends array of strings + Enderecos []string `json:"enderecos"` // Frontend sends array of strings + + // Ignored fields sent by frontend to prevent "unknown field" errors + ID interface{} `json:"id,omitempty"` + EmailVerified interface{} `json:"email_verified,omitempty"` + CreatedAt interface{} `json:"created_at,omitempty"` + UpdatedAt interface{} `json:"updated_at,omitempty"` + Nome interface{} `json:"nome,omitempty"` + Ativo interface{} `json:"ativo,omitempty"` + CPF interface{} `json:"cpf,omitempty"` + NomeSocial interface{} `json:"nome-social,omitempty"` + RegistroCompleto interface{} `json:"registro-completo,omitempty"` + Nivel interface{} `json:"nivel,omitempty"` } type requester struct { + ID uuid.UUID Role string CompanyID *uuid.UUID } @@ -120,6 +135,27 @@ type registerCompanyRequest struct { Longitude float64 `json:"longitude"` City string `json:"city"` State string `json:"state"` + + // Portuguese Frontend Compatibility + RazaoSocial string `json:"razao-social"` + NomeFantasia string `json:"nome-fantasia"` + DataAbertura string `json:"data-abertura"` // Fixed: frontend sends hyphen + Telefone string `json:"telefone"` + CodigoAtividade string `json:"codigo_atividade"` + DescricaoAtividade string `json:"descricao_atividade"` + Situacao string `json:"situacao"` // Ignored for now + NaturezaJuridica string `json:"natureza-juridica"` // Ignored for now + Porte string `json:"porte"` // Ignored for now + AtividadePrincipal string `json:"atividade-principal"` // Frontend might send this + AtividadePrincipalCodigo string `json:"atividade-principal-codigo"` // Frontend sends this + AtividadePrincipalDesc string `json:"atividade-principal-desc"` // Frontend sends this + Email string `json:"email"` // Frontend sends this + CapitalSocial float64 `json:"capital-social"` // Frontend sends this (number) + AddressID string `json:"enderecoID"` // Frontend sends this + TipoFrete string `json:"tipoFrete"` // Frontend sends this + RaioEntregaKm float64 `json:"raioEntregaKm"` // Frontend sends this + TaxaEntrega float64 `json:"taxaEntrega"` // Frontend sends this + ValorFreteKm float64 `json:"valorFreteKm"` // Frontend sends this } type updateCompanyRequest struct { @@ -195,12 +231,24 @@ type shippingCalculateRequest struct { PostalCode string `json:"postal_code,omitempty"` } +type createAddressRequest struct { + Title string `json:"titulo"` + ZipCode string `json:"cep"` + Street string `json:"logradouro"` + Number string `json:"numero"` + Complement string `json:"complemento"` + District string `json:"bairro"` + City string `json:"cidade"` + State string `json:"estado"` // JSON from frontend sends "estado" + Country string `json:"pais"` // JSON includes "pais" +} + // --- Utility Functions --- func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(v) + _ = jsonAPI.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, err error) { @@ -211,7 +259,7 @@ func decodeJSON(ctx context.Context, r *http.Request, v any) error { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() - dec := json.NewDecoder(r.Body) + dec := jsonAPI.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(v); err != nil { return err @@ -276,7 +324,7 @@ func parsePagination(r *http.Request) (int, int) { func getRequester(r *http.Request) (requester, error) { if claims, ok := middleware.GetClaims(r.Context()); ok { - return requester{Role: claims.Role, CompanyID: claims.CompanyID}, nil + return requester{ID: claims.UserID, Role: claims.Role, CompanyID: claims.CompanyID}, nil } role := r.Header.Get("X-User-Role") if role == "" { diff --git a/backend-old/internal/http/handler/financial_handler.go b/backend-old/internal/http/handler/financial_handler.go index 8452b45..276b119 100644 --- a/backend-old/internal/http/handler/financial_handler.go +++ b/backend-old/internal/http/handler/financial_handler.go @@ -17,7 +17,7 @@ func (h *Handler) UploadDocument(w http.ResponseWriter, r *http.Request) { Type string `json:"type"` URL string `json:"url"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := jsonAPI.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -29,7 +29,7 @@ func (h *Handler) UploadDocument(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(doc) + jsonAPI.NewEncoder(w).Encode(doc) } // GetDocuments lists company KYC docs. @@ -47,7 +47,7 @@ func (h *Handler) GetDocuments(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(docs) + jsonAPI.NewEncoder(w).Encode(docs) } // GetLedger returns financial history. @@ -68,7 +68,7 @@ func (h *Handler) GetLedger(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(res) + jsonAPI.NewEncoder(w).Encode(res) } // GetBalance returns current wallet balance. @@ -86,7 +86,7 @@ func (h *Handler) GetBalance(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]int64{"balance_cents": bal}) + jsonAPI.NewEncoder(w).Encode(map[string]int64{"balance_cents": bal}) } // RequestWithdrawal initiates a payout. @@ -101,7 +101,7 @@ func (h *Handler) RequestWithdrawal(w http.ResponseWriter, r *http.Request) { AmountCents int64 `json:"amount_cents"` BankInfo string `json:"bank_info"` } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := jsonAPI.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -113,7 +113,7 @@ func (h *Handler) RequestWithdrawal(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(wd) + jsonAPI.NewEncoder(w).Encode(wd) } // ListWithdrawals shows history of payouts. @@ -131,5 +131,5 @@ func (h *Handler) ListWithdrawals(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(wds) + jsonAPI.NewEncoder(w).Encode(wds) } diff --git a/backend-old/internal/http/handler/handler.go b/backend-old/internal/http/handler/handler.go index d68cf86..e402e0f 100644 --- a/backend-old/internal/http/handler/handler.go +++ b/backend-old/internal/http/handler/handler.go @@ -5,6 +5,8 @@ import ( "database/sql" "errors" "net/http" + "strconv" + "time" jsoniter "github.com/json-iterator/go" @@ -15,7 +17,7 @@ import ( "github.com/saveinmed/backend-go/internal/usecase" ) -var json = jsoniter.ConfigCompatibleWithStandardLibrary +var jsonAPI = jsoniter.ConfigCompatibleWithStandardLibrary type Handler struct { svc *usecase.Service @@ -72,9 +74,19 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) { Email: req.Email, } + // If no company provided, create a placeholder one to satisfy DB constraints if user.CompanyID == uuid.Nil && company == nil { - writeError(w, http.StatusBadRequest, errors.New("company_id or company payload is required")) - return + timestamp := time.Now().UnixNano() + company = &domain.Company{ + // ID left as Nil so usecase creates it + Category: "farmacia", + CNPJ: "TMP-" + strconv.FormatInt(timestamp, 10), // Temporary CNPJ + CorporateName: "Empresa de " + req.Name, + LicenseNumber: "PENDING", + IsVerified: false, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } } if err := h.svc.RegisterAccount(r.Context(), company, user, req.Password); err != nil { @@ -123,6 +135,52 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, authResponse{Token: token, ExpiresAt: exp}) } +// GetMe godoc +// @Summary Obter dados do usuário logado +// @Tags Autenticação +// @Security BearerAuth +// @Produce json +// @Success 200 {object} domain.User +// @Failure 401 {object} map[string]string +// @Router /api/v1/auth/me [get] +func (h *Handler) GetMe(w http.ResponseWriter, r *http.Request) { + requester, err := getRequester(r) + if err != nil { + writeError(w, http.StatusUnauthorized, err) + return + } + + user, err := h.svc.GetUser(r.Context(), requester.ID) + if err != nil { + writeError(w, http.StatusNotFound, err) + return + } + + var companyName string + var isSuperAdmin bool + + if user.CompanyID != uuid.Nil { + if c, err := h.svc.GetCompany(r.Context(), user.CompanyID); err == nil && c != nil { + companyName = c.CorporateName + if c.Category == "admin" { + isSuperAdmin = true + } + } + } + + response := struct { + *domain.User + CompanyName string `json:"company_name"` + SuperAdmin bool `json:"superadmin"` + }{ + User: user, + CompanyName: companyName, + SuperAdmin: isSuperAdmin, + } + + writeJSON(w, http.StatusOK, response) +} + // RegisterCustomer godoc // @Summary Cadastro de cliente // @Description Cria um usuário do tipo cliente e opcionalmente uma empresa, retornando token JWT. diff --git a/backend-old/internal/http/handler/handler_test.go b/backend-old/internal/http/handler/handler_test.go index 2bfc302..050c87f 100644 --- a/backend-old/internal/http/handler/handler_test.go +++ b/backend-old/internal/http/handler/handler_test.go @@ -43,6 +43,11 @@ func NewMockRepository() *MockRepository { } } +func (m *MockRepository) CreateAddress(ctx context.Context, address *domain.Address) error { + address.ID = uuid.Must(uuid.NewV7()) + return nil +} + // Company methods func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error { id, _ := uuid.NewV7() diff --git a/backend-old/internal/http/handler/user_handler.go b/backend-old/internal/http/handler/user_handler.go index 4729c0c..a6a060f 100644 --- a/backend-old/internal/http/handler/user_handler.go +++ b/backend-old/internal/http/handler/user_handler.go @@ -199,6 +199,14 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { if req.CompanyID != nil { user.CompanyID = *req.CompanyID } + // Map frontend's array of company IDs to the single CompanyID + if len(req.EmpresasDados) > 0 { + // Use the first company ID from the list + if id, err := uuid.FromString(req.EmpresasDados[0]); err == nil { + user.CompanyID = id + } + } + if req.Role != nil { user.Role = *req.Role } diff --git a/backend-old/internal/http/middleware/cors.go b/backend-old/internal/http/middleware/cors.go index 0c660ce..6f75749 100644 --- a/backend-old/internal/http/middleware/cors.go +++ b/backend-old/internal/http/middleware/cors.go @@ -36,7 +36,8 @@ func CORSWithConfig(cfg CORSConfig) func(http.Handler) http.Handler { } w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept") + w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Max-Age", "86400") // Handle preflight requests diff --git a/backend-old/internal/repository/postgres/migrations/0005_tenants_operating_hours.sql b/backend-old/internal/repository/postgres/migrations/0005_tenants_operating_hours.sql index 2585f85..017da21 100644 --- a/backend-old/internal/repository/postgres/migrations/0005_tenants_operating_hours.sql +++ b/backend-old/internal/repository/postgres/migrations/0005_tenants_operating_hours.sql @@ -3,7 +3,4 @@ ALTER TABLE companies ADD COLUMN phone TEXT NOT NULL DEFAULT ''; ALTER TABLE companies ADD COLUMN operating_hours TEXT NOT NULL DEFAULT ''; ALTER TABLE companies ADD COLUMN is_24_hours BOOLEAN NOT NULL DEFAULT false; --- +goose Down -ALTER TABLE companies DROP COLUMN phone; -ALTER TABLE companies DROP COLUMN operating_hours; -ALTER TABLE companies DROP COLUMN is_24_hours; + diff --git a/backend-old/internal/repository/postgres/migrations/0006_product_catalog_fields.sql b/backend-old/internal/repository/postgres/migrations/0006_product_catalog_fields.sql index addebc2..ceea8f3 100644 --- a/backend-old/internal/repository/postgres/migrations/0006_product_catalog_fields.sql +++ b/backend-old/internal/repository/postgres/migrations/0006_product_catalog_fields.sql @@ -6,9 +6,4 @@ ALTER TABLE products ADD COLUMN IF NOT EXISTS category TEXT NOT NULL DEFAULT ''; ALTER TABLE products ADD COLUMN IF NOT EXISTS subcategory TEXT NOT NULL DEFAULT ''; ALTER TABLE products ADD COLUMN IF NOT EXISTS observations TEXT NOT NULL DEFAULT ''; --- +goose Down -ALTER TABLE products DROP COLUMN IF EXISTS ean_code; -ALTER TABLE products DROP COLUMN IF EXISTS manufacturer; -ALTER TABLE products DROP COLUMN IF EXISTS category; -ALTER TABLE products DROP COLUMN IF EXISTS subcategory; -ALTER TABLE products DROP COLUMN IF EXISTS observations; + diff --git a/backend-old/internal/repository/postgres/migrations/0004_users_email_verified.sql b/backend-old/internal/repository/postgres/migrations/0010_users_email_verified.sql similarity index 100% rename from backend-old/internal/repository/postgres/migrations/0004_users_email_verified.sql rename to backend-old/internal/repository/postgres/migrations/0010_users_email_verified.sql diff --git a/backend-old/internal/repository/postgres/migrations/0011_create_addresses_table.sql b/backend-old/internal/repository/postgres/migrations/0011_create_addresses_table.sql new file mode 100644 index 0000000..47ca7fe --- /dev/null +++ b/backend-old/internal/repository/postgres/migrations/0011_create_addresses_table.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS addresses ( + id UUID PRIMARY KEY, + entity_id UUID NOT NULL, -- UserID or CompanyID + title TEXT NOT NULL, + zip_code TEXT NOT NULL, + street TEXT NOT NULL, + number TEXT NOT NULL, + complement TEXT, + district TEXT NOT NULL, + city TEXT NOT NULL, + state TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_addresses_entity_id ON addresses(entity_id); diff --git a/backend-old/internal/repository/postgres/postgres.go b/backend-old/internal/repository/postgres/postgres.go index 287b0d8..f8b29b0 100644 --- a/backend-old/internal/repository/postgres/postgres.go +++ b/backend-old/internal/repository/postgres/postgres.go @@ -57,6 +57,10 @@ func (r *Repository) ListCompanies(ctx context.Context, filter domain.CompanyFil clauses = append(clauses, fmt.Sprintf("state = $%d", len(args)+1)) args = append(args, filter.State) } + if filter.IsVerified != nil { + clauses = append(clauses, fmt.Sprintf("is_verified = $%d", len(args)+1)) + args = append(args, *filter.IsVerified) + } where := "" if len(clauses) > 0 { @@ -1294,3 +1298,11 @@ account_id = EXCLUDED.account_id, account_type = EXCLUDED.account_type, status = _, err := r.db.NamedExecContext(ctx, query, account) return err } + +func (r *Repository) CreateAddress(ctx context.Context, address *domain.Address) error { + query := `INSERT INTO addresses (id, entity_id, title, zip_code, street, number, complement, district, city, state, created_at, updated_at) + VALUES (:id, :entity_id, :title, :zip_code, :street, :number, :complement, :district, :city, :state, :created_at, :updated_at)` + + _, err := r.db.NamedExecContext(ctx, query, address) + return err +} diff --git a/backend-old/internal/server/server.go b/backend-old/internal/server/server.go index 9c7f525..f4d1910 100644 --- a/backend-old/internal/server/server.go +++ b/backend-old/internal/server/server.go @@ -64,10 +64,14 @@ func New(cfg config.Config) (*Server, error) { auth := middleware.RequireAuth([]byte(cfg.JWTSecret)) adminOnly := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin") + // Companies (Empresas) mux.Handle("POST /api/v1/companies", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) + mux.Handle("POST /api/v1/empresas", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("GET /api/v1/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip)) + mux.Handle("GET /api/v1/empresas", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("GET /api/v1/companies/{id}", chain(http.HandlerFunc(h.GetCompany), middleware.Logger, middleware.Gzip)) mux.Handle("PATCH /api/v1/companies/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip)) + mux.Handle("PATCH /api/v1/empresas/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("DELETE /api/v1/companies/{id}", chain(http.HandlerFunc(h.DeleteCompany), middleware.Logger, middleware.Gzip)) mux.Handle("PATCH /api/v1/companies/{id}/verify", chain(http.HandlerFunc(h.VerifyCompany), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("GET /api/v1/companies/me", chain(http.HandlerFunc(h.GetMyCompany), middleware.Logger, middleware.Gzip, auth)) @@ -135,13 +139,14 @@ func New(cfg config.Config) (*Server, error) { mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip)) + mux.Handle("GET /api/v1/auth/me", chain(http.HandlerFunc(h.GetMe), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/auth/logout", chain(http.HandlerFunc(h.Logout), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/password/forgot", chain(http.HandlerFunc(h.ForgotPassword), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/password/reset", chain(http.HandlerFunc(h.ResetPassword), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/refresh-token", chain(http.HandlerFunc(h.RefreshToken), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/verify-email", chain(http.HandlerFunc(h.VerifyEmail), middleware.Logger, middleware.Gzip)) - - // Push Notifications (FCM) + // Address + mux.Handle("POST /api/v1/enderecos", chain(http.HandlerFunc(h.CreateAddress), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/push/register", chain(http.HandlerFunc(h.RegisterPushToken), middleware.Logger, middleware.Gzip, auth)) mux.Handle("DELETE /api/v1/push/unregister", chain(http.HandlerFunc(h.UnregisterPushToken), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/push/test", chain(http.HandlerFunc(h.TestPushNotification), middleware.Logger, middleware.Gzip, auth)) @@ -150,6 +155,8 @@ func New(cfg config.Config) (*Server, error) { mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/users/", chain(http.HandlerFunc(h.GetUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) + mux.Handle("PATCH /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Add PATCH support + mux.Handle("PATCH /api/v1/usuarios/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Alias for frontend mux.Handle("DELETE /api/v1/users/", chain(http.HandlerFunc(h.DeleteUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/cart", chain(http.HandlerFunc(h.AddToCart), middleware.Logger, middleware.Gzip, auth)) @@ -186,7 +193,7 @@ func (s *Server) Start(ctx context.Context) error { // 1. Create/Get Admin Company adminCNPJ := "00000000000000" company := &domain.Company{ - ID: uuid.Must(uuid.NewV7()), + ID: uuid.Nil, CNPJ: adminCNPJ, CorporateName: "SaveInMed Admin", Category: "admin", @@ -231,6 +238,10 @@ func (s *Server) Start(ctx context.Context) error { // For now, let's log error but not fail startup hard, or fail hard to signal issue. log.Printf("Failed to seed admin: %v", err) } else { + // FORCE VERIFY the admin company + if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil { + log.Printf("Failed to verify admin company: %v", err) + } log.Printf("Admin user created successfully") } } else { diff --git a/backend-old/internal/usecase/usecase.go b/backend-old/internal/usecase/usecase.go index 354e8dc..b88be10 100644 --- a/backend-old/internal/usecase/usecase.go +++ b/backend-old/internal/usecase/usecase.go @@ -80,6 +80,8 @@ type Repository interface { UpsertPaymentGatewayConfig(ctx context.Context, config *domain.PaymentGatewayConfig) error GetSellerPaymentAccount(ctx context.Context, sellerID uuid.UUID) (*domain.SellerPaymentAccount, error) UpsertSellerPaymentAccount(ctx context.Context, account *domain.SellerPaymentAccount) error + + CreateAddress(ctx context.Context, address *domain.Address) error } // PaymentGateway abstracts Mercado Pago integration. @@ -784,6 +786,18 @@ func (s *Service) Authenticate(ctx context.Context, identifier, password string) return "", time.Time{}, errors.New("invalid credentials") } + // Check if company is verified + if user.CompanyID != uuid.Nil { + company, err := s.repo.GetCompany(ctx, user.CompanyID) + if err != nil { + // If company not found, something is wrong with data integrity, prevent login + return "", time.Time{}, errors.New("associated company not found") + } + if !company.IsVerified { + return "", time.Time{}, errors.New("account pending approval") + } + } + return s.issueAccessToken(user) } @@ -997,3 +1011,8 @@ func (s *Service) GetShippingSettings(ctx context.Context, vendorID uuid.UUID) ( func (s *Service) UpsertShippingSettings(ctx context.Context, settings *domain.ShippingSettings) error { return s.repo.UpsertShippingSettings(ctx, settings) } + +func (s *Service) CreateAddress(ctx context.Context, address *domain.Address) error { + address.ID = uuid.Must(uuid.NewV7()) + return s.repo.CreateAddress(ctx, address) +} diff --git a/backend-old/internal/usecase/usecase_test.go b/backend-old/internal/usecase/usecase_test.go index 1903d9d..1f1e6fa 100644 --- a/backend-old/internal/usecase/usecase_test.go +++ b/backend-old/internal/usecase/usecase_test.go @@ -47,6 +47,14 @@ func NewMockRepository() *MockRepository { } } +// Address methods +func (m *MockRepository) CreateAddress(ctx context.Context, address *domain.Address) error { + address.ID = uuid.Must(uuid.NewV7()) + address.CreatedAt = time.Now() + address.UpdatedAt = time.Now() + return nil +} + // Company methods func (m *MockRepository) CreateCompany(ctx context.Context, company *domain.Company) error { company.CreatedAt = time.Now() diff --git a/backend-old/login.json b/backend-old/login.json new file mode 100644 index 0000000..b410fec --- /dev/null +++ b/backend-old/login.json @@ -0,0 +1 @@ +{"email":"andre.fr93@gmail.com","password":"teste1234"} diff --git a/saveinmed-frontend/src/app/api/cep/[cep]/route.ts b/saveinmed-frontend/src/app/api/cep/[cep]/route.ts index 659bea1..3ff5222 100644 --- a/saveinmed-frontend/src/app/api/cep/[cep]/route.ts +++ b/saveinmed-frontend/src/app/api/cep/[cep]/route.ts @@ -19,7 +19,8 @@ export async function GET( ); } - const token = process.env.CEP_API_TOKEN; + // Token fornecido pelo usuário + const token = "9426cdf7a6f36931f5afba5a3c4e7bf29974fec6d2662ebcb6d7a1b237ffacdc"; // Fazer a requisição para a API AwesomeAPI const response = await fetch( diff --git a/saveinmed-frontend/src/app/completar-registro/page.tsx b/saveinmed-frontend/src/app/completar-registro/page.tsx index 77a43a2..f698f14 100644 --- a/saveinmed-frontend/src/app/completar-registro/page.tsx +++ b/saveinmed-frontend/src/app/completar-registro/page.tsx @@ -191,6 +191,7 @@ const CompletarRegistroPage = () => { console.log("📝 Campos pré-preenchidos com dados do usuário"); } else { console.log("ℹ️ Não foi possível carregar dados do usuário"); + // Do not remove token here blindly } } else { console.log("ℹ️ Nenhum token encontrado, formulário será preenchido manualmente"); diff --git a/saveinmed-frontend/src/app/login/page.tsx b/saveinmed-frontend/src/app/login/page.tsx index 8bcde0c..38c6be1 100644 --- a/saveinmed-frontend/src/app/login/page.tsx +++ b/saveinmed-frontend/src/app/login/page.tsx @@ -67,11 +67,15 @@ const LoginPageContent = () => { if (response.ok) { const userData = await response.json(); - router.push("/dashboard"); + // ... (log logic) } else { const errorText = await response.text(); - // Limpar token inválido - localStorage.removeItem('access_token'); + console.log("❌ Falha no /me:", errorText); + + // Only remove token if explicitly unauthorized (401) + if (response.status === 401) { + localStorage.removeItem('access_token'); + } } } catch (error) { } finally { @@ -102,8 +106,8 @@ const LoginPageContent = () => { credentials: 'include', // Permite que o browser receba e armazene cookies mode: 'cors', // Habilita CORS explicitamente body: JSON.stringify({ - identificador: email, - senha: password + email: email, + password: password }) }); @@ -236,13 +240,11 @@ const LoginPageContent = () => { 'Content-Type': 'application/json', }, body: JSON.stringify({ - identificador: email, // identificador = email do usuário (usado no login) + role: "Seller", + name: name, + username: email, email: email, - nome: name, // nome = nome completo do usuário - senha: password, - nivel: "admin", // valor estático - superadmin: false, // valor estático - "registro-completo": false // valor estático - registro incompleto por padrão + password: password }) }); @@ -278,8 +280,8 @@ const LoginPageContent = () => { credentials: 'include', mode: 'cors', body: JSON.stringify({ - identificador: email, - senha: password + email: email, + password: password }) }); diff --git a/saveinmed-frontend/src/app/usuarios-pendentes/page.tsx b/saveinmed-frontend/src/app/usuarios-pendentes/page.tsx index d693d6a..700111a 100644 --- a/saveinmed-frontend/src/app/usuarios-pendentes/page.tsx +++ b/saveinmed-frontend/src/app/usuarios-pendentes/page.tsx @@ -1,649 +1,173 @@ "use client"; -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; +import { EmpresaBff, empresaApiService } from "@/services/empresaApiService"; import { useRouter } from "next/navigation"; import Header from "@/components/Header"; +import { CheckCircleIcon, XCircleIcon, BuildingOfficeIcon } from "@heroicons/react/24/outline"; -interface Usuario { - id: string; - identificador: string; - nome: string; - email: string; - telefone?: string; - cpf?: string; - ativo: boolean; - superadmin: boolean; - nivel: string; - registro_completo: boolean; - enderecos: string[]; - empresas_dados: string[]; - createdAt: string; - updatedAt: string; -} - -interface Endereco { - id: string; - cep: string; - logradouro: string; - numero: string; - complemento?: string; - bairro: string; - cidade: string; - estado: string; -} - -interface EmpresaDados { - id: string; - cnpj: string; - "razao-social": string; - "nome-fantasia": string; - "data-abertura": string; - situacao: string; - "natureza-juridica": string; - porte: string; - "capital-social": number; - telefone: string; - email: string; - "atividade-principal-codigo": string; - "atividade-principal-desc": string; - enderecos: string[]; -} - -interface UsuarioCompleto extends Usuario { - enderecoData?: Endereco; - empresaData?: EmpresaDados; -} - -const UsuariosPendentesPage = () => { +export default function UsuariosPendentesPage() { const router = useRouter(); - const [usuarios, setUsuarios] = useState([]); + const [empresas, setEmpresas] = useState([]); const [loading, setLoading] = useState(true); + const [userData, setUserData] = useState(null); const [error, setError] = useState(""); - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [selectedUser, setSelectedUser] = useState(null); - const [showDetailsModal, setShowDetailsModal] = useState(false); - const [activatingUserId, setActivatingUserId] = useState(null); - const [currentUser, setCurrentUser] = useState(null); // Usuário logado - // Carregar dados do usuário logado - useEffect(() => { - const userData = localStorage.getItem('user'); - if (userData) { - try { - setCurrentUser(JSON.parse(userData)); - } catch (error) { - console.error('Erro ao parsing dos dados do usuário:', error); - } - } - }, []); - - // Buscar usuários pendentes - const fetchUsuarios = async (pageNum: number = 1) => { + const fetchUserData = async () => { try { - setLoading(true); - const token = localStorage.getItem('access_token'); - - if (!token) { - router.push('/login'); + const storedToken = localStorage.getItem('access_token'); + if (!storedToken) { + router.push("/login"); return; } - const response = await fetch( - `${process.env.NEXT_PUBLIC_BFF_API_URL}/usuarios?page=${pageNum}&ativo=false`, - { - method: 'GET', - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - } - ); + const response = await fetch(`${process.env.NEXT_PUBLIC_BFF_API_URL}/auth/me`, { + method: "GET", + headers: { + "accept": "application/json", + "Authorization": `Bearer ${storedToken}`, + }, + }); if (!response.ok) { - throw new Error('Erro ao buscar usuários pendentes'); + localStorage.removeItem('access_token'); + router.push("/login"); + return; } - const data = await response.json(); + const userResponse = await response.json(); + setUserData(userResponse); + } catch (error) { + console.error("Erro ao carregar dados do usuário:", error); + } + }; - const usuariosData = data.data || data.items || data; - - // Para cada usuário, buscar dados do endereço e empresa - const usuariosCompletos = await Promise.all( - usuariosData.map(async (usuario: Usuario) => { - const usuarioCompleto: UsuarioCompleto = { ...usuario }; - - // Buscar dados do endereço - if (usuario.enderecos && usuario.enderecos.length > 0) { - try { - const enderecoResponse = await fetch( - `${process.env.NEXT_PUBLIC_BFF_API_URL}/enderecos/${usuario.enderecos[0]}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - } - ); - if (enderecoResponse.ok) { - usuarioCompleto.enderecoData = await enderecoResponse.json(); - } - } catch (error) { - console.error('Erro ao buscar endereço:', error); - } - } - - // Buscar dados da empresa - if (usuario.empresas_dados && usuario.empresas_dados.length > 0) { - try { - const empresaResponse = await fetch( - `${process.env.NEXT_PUBLIC_BFF_API_URL}/empresas/${usuario.empresas_dados[0]}`, - { - headers: { - 'accept': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - } - ); - if (empresaResponse.ok) { - usuarioCompleto.empresaData = await empresaResponse.json(); - } - } catch (error) { - console.error('Erro ao buscar empresa:', error); - } - } - - return usuarioCompleto; - }) - ); - - setUsuarios(usuariosCompletos); - - // Se há paginação na resposta - if (data.pagination) { - setTotalPages(data.pagination.totalPages || 1); - } - - } catch (error: any) { - console.error('❌ Erro ao carregar usuários:', error); - setError(error.message || 'Erro ao carregar usuários pendentes'); + const fetchPendentes = async () => { + setLoading(true); + setError(""); + try { + const data = await empresaApiService.listar({ is_verified: false }); + setEmpresas(data); + } catch (err) { + console.error(err); + setError("Erro ao carregar usuários pendentes."); } finally { setLoading(false); } }; - // Ativar usuário - const ativarUsuario = async (usuarioId: string) => { + useEffect(() => { + fetchUserData(); + fetchPendentes(); + }, []); + + const handleAprovar = async (companyId: string) => { + if (!confirm("Tem certeza que deseja aprovar este cadastro?")) return; + try { - setActivatingUserId(usuarioId); - const token = localStorage.getItem('access_token'); + const sucesso = await empresaApiService.atualizar(companyId, { + is_verified: true, + } as any); - const response = await fetch( - `${process.env.NEXT_PUBLIC_BFF_API_URL}/usuarios/${usuarioId}`, - { - method: 'PATCH', - headers: { - 'accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ - ativo: true - }), - } - ); - - if (!response.ok) { - throw new Error('Erro ao ativar usuário'); + if (sucesso) { + alert("Cadastro aprovado com sucesso!"); + fetchPendentes(); + } else { + alert("Erro ao aprovar cadastro."); } - - // Recarregar lista - await fetchUsuarios(page); - - // Fechar modal se estiver aberto - if (showDetailsModal && selectedUser?.id === usuarioId) { - setShowDetailsModal(false); - setSelectedUser(null); - } - - } catch (error: any) { - console.error('❌ Erro ao ativar usuário:', error); - setError(error.message || 'Erro ao ativar usuário'); - } finally { - setActivatingUserId(null); + } catch (err) { + console.error(err); + alert("Erro ao processar aprovação."); } }; - // Ver detalhes do usuário - const verDetalhes = (usuario: UsuarioCompleto) => { - setSelectedUser(usuario); - setShowDetailsModal(true); - }; - - // Fechar modal de detalhes - const closeDetailsModal = () => { - setShowDetailsModal(false); - setSelectedUser(null); - }; - - // Carregar usuários ao montar componente - useEffect(() => { - fetchUsuarios(page); - }, [page]); + if (!userData && loading) { + return ( +
+
+
+

Carregando...

+
+
+ ); + } return (
-
+
-
- {/* Cabeçalho da página */} -
-
-
-

Usuários Pendentes

-

- Gerencie usuários aguardando aprovação de cadastro -

-
- -
-
- - {/* Conteúdo */} - {loading ? ( -
-
-

Carregando usuários pendentes...

-
- ) : error ? ( -
-
- - - -

{error}

-
-
- ) : usuarios.length === 0 ? ( -
-
- - - -
-

- Nenhum usuário pendente -

-

- Todos os usuários foram processados ou não há cadastros aguardando aprovação. -

-
- ) : ( -
- {/* Tabela de usuários */} -
- - - - - - - - - - - - {usuarios.map((usuario) => ( - - - - - - - - ))} - -
- Usuário - - Empresa - - Data do Cadastro - - Status - - Ações -
-
-
- - {usuario.nome?.charAt(0)?.toUpperCase() || 'U'} - -
-
-
- {usuario.nome} -
-
- {usuario.email} -
-
-
-
-
- {usuario.empresaData?.['razao-social'] || 'Não informado'} -
-
- {usuario.empresaData?.cnpj || ''} -
-
-
- {new Date(usuario.createdAt).toLocaleDateString('pt-BR')} -
-
- {new Date(usuario.createdAt).toLocaleTimeString('pt-BR', { - hour: '2-digit', - minute: '2-digit' - })} -
-
- - - - - Pendente - - -
- - -
-
-
- - {/* Paginação */} - {totalPages > 1 && ( -
-
- - -
-
-
-

- Página {page} de{' '} - {totalPages} -

-
-
- -
-
+
+ + {error && ( +
+
+ +
+

{error}

- )} +
)} -
- {/* Modal de Detalhes do Usuário */} - {showDetailsModal && selectedUser && ( -
-
- {/* Header do Modal (sticky para não sumir ao rolar) */} -
-
-

- Detalhes do Usuário -

- -
-
- - {/* Conteúdo do Modal - scrollable */} -
- {/* Informações Pessoais */} -
-

- Informações Pessoais -

-
-
- -

{selectedUser.nome}

-
-
- -

{selectedUser.email}

-
-
- -

{selectedUser.cpf || 'Não informado'}

-
-
- -

{selectedUser.telefone || 'Não informado'}

-
-
- -

{selectedUser.nivel}

-
-
- -

- {new Date(selectedUser.createdAt).toLocaleString('pt-BR')} -

-
-
-
- - {/* Endereço */} - {selectedUser.enderecoData && ( -
-

- Endereço -

-
-
- -

{selectedUser.enderecoData.cep}

-
-
- -

- {selectedUser.enderecoData.logradouro}, {selectedUser.enderecoData.numero} -

-
-
- -

{selectedUser.enderecoData.complemento || 'Não informado'}

-
-
- -

{selectedUser.enderecoData.bairro}

-
-
- -

{selectedUser.enderecoData.cidade}

-
-
- -

{selectedUser.enderecoData.estado}

-
-
-
- )} - - {/* Empresa */} - {selectedUser.empresaData && ( -
-

- Dados da Empresa -

-
-
- -

{selectedUser.empresaData['razao-social']}

-
-
- -

{selectedUser.empresaData['nome-fantasia'] || 'Não informado'}

-
-
- -

{selectedUser.empresaData.cnpj}

-
-
- -

{selectedUser.empresaData['data-abertura'] || 'Não informado'}

-
-
- -

{selectedUser.empresaData.situacao || 'Não informado'}

-
-
- -

{selectedUser.empresaData['natureza-juridica'] || 'Não informado'}

-
-
- -

{selectedUser.empresaData.porte || 'Não informado'}

-
-
- -

- {selectedUser.empresaData['capital-social'] - ? `R$ ${selectedUser.empresaData['capital-social'].toLocaleString('pt-BR')}` - : 'Não informado'} -

-
-
- -

{selectedUser.empresaData.telefone || 'Não informado'}

-
-
- -

{selectedUser.empresaData.email || 'Não informado'}

-
-
- -

- {selectedUser.empresaData['atividade-principal-codigo'] || selectedUser.empresaData['atividade-principal-desc'] - ? `${selectedUser.empresaData['atividade-principal-codigo']} - ${selectedUser.empresaData['atividade-principal-desc']}` - : 'Não informado'} -

-
-
-
- )} -
- - {/* Footer do Modal (sticky para ficar visível) */} -
- - -
+ {loading ? ( +
+
+

Carregando solicitações...

-
- )} + ) : empresas.length === 0 ? ( +
+ +

Nenhum cadastro pendente

+

Todas as solicitações foram processadas.

+
+ ) : ( +
+ {empresas.map((empresa) => ( +
+
+
+
+
+

+ {empresa.razao_social || empresa.nome_fantasia || "Sem Nome"} +

+ + Pendente + +
+ +
+

CNPJ: {empresa.cnpj}

+

Email: {empresa.email}

+

Telefone: {empresa.telefone}

+

ID: {empresa.id}

+
+
+ +
+ +
+
+
+
+ ))} +
+ )} +
); -}; - -export default UsuariosPendentesPage; \ No newline at end of file +} \ No newline at end of file diff --git a/saveinmed-frontend/src/components/Header.tsx b/saveinmed-frontend/src/components/Header.tsx index ea61c52..32ec187 100644 --- a/saveinmed-frontend/src/components/Header.tsx +++ b/saveinmed-frontend/src/components/Header.tsx @@ -44,6 +44,7 @@ const Header = ({ user?.name || "Usuário"; const displayCompanyName = + user?.company_name || user?.empresa?.["nome-fantasia"] || user?.empresa?.["razao-social"] || user?.empresa?.nomeFantasia || @@ -228,7 +229,10 @@ const Header = ({ {isOpen && ( -
    + <> + {/* Bridge do prevent closing */} +
    +
    • @@ -315,6 +319,7 @@ const Header = ({
    + )}
diff --git a/saveinmed-frontend/src/services/empresaApiService.ts b/saveinmed-frontend/src/services/empresaApiService.ts index 13313a6..28407fd 100644 --- a/saveinmed-frontend/src/services/empresaApiService.ts +++ b/saveinmed-frontend/src/services/empresaApiService.ts @@ -58,6 +58,52 @@ export const empresaApiService = { } }, + /** + * Lista empresas com filtros + * @param filters - Filtros (ex: is_verified=false) + * @returns Lista de empresas + */ + listar: async (filters?: Record): Promise => { + try { + const token = localStorage.getItem('access_token'); + if (!token) throw new Error('Token não encontrado'); + + const querySource = filters || {}; + const queryParams = new URLSearchParams(); + Object.keys(querySource).forEach(key => { + if (querySource[key] !== undefined && querySource[key] !== null) { + queryParams.append(key, String(querySource[key])); + } + }); + + const response = await fetch(`${BFF_BASE_URL}/empresas?${queryParams.toString()}`, { + method: 'GET', + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + console.error(`❌ Erro ao listar empresas: ${response.status}`); + return []; + } + + const data = await response.json(); + const items = data.tenants || (Array.isArray(data) ? data : []); + + // Mapear campos do backend para o frontend + return items.map((item: any) => ({ + ...item, + razao_social: item.corporate_name || item.razao_social, // Fallback + nome_fantasia: item.trade_name || item.nome_fantasia || "", + })); + } catch (error) { + console.error('❌ Erro ao listar empresas:', error); + return []; + } + }, + /** * Atualiza dados da empresa * @param empresaId - ID da empresa diff --git a/saveinmed-frontend/src/types/auth.ts b/saveinmed-frontend/src/types/auth.ts index 5c6a2a5..98c10b1 100644 --- a/saveinmed-frontend/src/types/auth.ts +++ b/saveinmed-frontend/src/types/auth.ts @@ -19,6 +19,7 @@ export interface UserData { "nome-social": string | null; cpf: string; email: string; + company_name?: string; nivel: UserRole; empresas?: any[]; enderecos?: any[];