From 9c6ee3afdb8f12570601e368c94e3946e385477d Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Mon, 9 Feb 2026 00:56:09 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20habilita=20edi=C3=A7=C3=A3o=20de=20perf?= =?UTF-8?q?il=20para=20clientes=20e=20corrige=20carga=20de=20dados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Adiciona endpoint `PUT /api/me` para permitir atualização de dados do usuário logado. - Implementa query `UpdateCadastroCliente` e função de serviço [UpdateClientData]para persistir alterações de clientes. - Atualiza handlers [Me], [Login] e [ListPending] para incluir e mapear corretamente campos de cliente (CPF, Endereço, Telefone). - Corrige mapeamento do campo `phone` na struct de resposta do usuário. Frontend: - Habilita o formulário de edição em [Profile.tsx] para usuários do tipo 'CLIENTE' (Event Owner). - Adiciona função [updateUserProfile] em [apiService.ts] para consumir o novo endpoint. - Atualiza [AuthContext] para persistir campos do cliente (CPF, Endereço, etc.) durante a restauração de sessão ([restoreSession], corrigindo o bug de perfil vazio ao recarregar a página. - Padroniza envio de dados no Registro e Aprovação para usar `snake_case` (ex: `cpf_cnpj`, `professional_type`). - Atualiza tipos em [types.ts] para incluir campos de endereço e documentos. --- backend/cmd/api/main.go | 1 + backend/internal/auth/handler.go | 265 ++++++++++++++---- backend/internal/auth/service.go | 162 ++++++++--- backend/internal/db/generated/models.go | 8 + backend/internal/db/generated/usuarios.sql.go | 151 +++++++++- .../migrations/017_add_client_fields.up.sql | 9 + backend/internal/db/queries/usuarios.sql | 39 ++- backend/internal/db/schema.sql | 10 + frontend/contexts/AuthContext.tsx | 85 ++++-- frontend/pages/ProfessionalRegister.tsx | 8 +- frontend/pages/Profile.tsx | 43 ++- frontend/pages/Register.tsx | 100 ++++++- frontend/pages/UserApproval.tsx | 195 ++++++++++--- frontend/services/apiService.ts | 36 +++ frontend/types.ts | 13 +- frontend/utils/masks.ts | 24 +- 16 files changed, 981 insertions(+), 168 deletions(-) create mode 100644 backend/internal/db/migrations/017_add_client_fields.up.sql diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index d5961d8..61fb383 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -173,6 +173,7 @@ func main() { api.Use(auth.AuthMiddleware(cfg)) { api.GET("/me", authHandler.Me) + api.PUT("/me", authHandler.UpdateMe) profGroup := api.Group("/profissionais") { diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 88f46a1..1b4d6eb 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -56,12 +56,20 @@ func (h *Handler) GetUploadURL(c *gin.Context) { type registerRequest struct { Email string `json:"email" binding:"required,email"` Senha string `json:"senha" binding:"required,min=6"` + Role string `json:"role" binding:"required"` Nome string `json:"nome" binding:"required"` Telefone string `json:"telefone"` - Role string `json:"role" binding:"required"` + TipoProfissional string `json:"professional_type"` EmpresaID string `json:"empresa_id"` - TipoProfissional string `json:"tipo_profissional"` // New field - Regiao string `json:"regiao"` // Optional: for AdminCreateUser override + Regiao string `json:"regiao"` + CpfCnpj string `json:"cpf_cnpj"` + Cep string `json:"cep"` + Endereco string `json:"endereco"` + Numero string `json:"numero"` + Complemento string `json:"complemento"` + Bairro string `json:"bairro"` + Cidade string `json:"cidade"` + Estado string `json:"estado"` } // Register godoc @@ -105,7 +113,7 @@ func (h *Handler) Register(c *gin.Context) { regiao = "SP" } - user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, req.TipoProfissional, empresaIDPtr, profData, regiao) + user, err := h.service.Register(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.Telefone, req.TipoProfissional, empresaIDPtr, profData, regiao, req.CpfCnpj, req.Cep, req.Endereco, req.Numero, req.Complemento, req.Bairro, req.Cidade, req.Estado) if err != nil { if strings.Contains(err.Error(), "duplicate key") { c.JSON(http.StatusConflict, gin.H{"error": "email already registered"}) @@ -177,6 +185,14 @@ type userResponse struct { CompanyID string `json:"company_id,omitempty"` CompanyName string `json:"company_name,omitempty"` AllowedRegions []string `json:"allowed_regions"` + CpfCnpj string `json:"cpf_cnpj,omitempty"` + Cep string `json:"cep,omitempty"` + Endereco string `json:"endereco,omitempty"` + Numero string `json:"numero,omitempty"` + Complemento string `json:"complemento,omitempty"` + Bairro string `json:"bairro,omitempty"` + Cidade string `json:"cidade,omitempty"` + Estado string `json:"estado,omitempty"` } // Login godoc @@ -233,22 +249,66 @@ func (h *Handler) Login(c *gin.Context) { companyName = user.EmpresaNome.String } - resp := loginResponse{ - AccessToken: tokenPair.AccessToken, - ExpiresAt: "2025-...", // logic to calculate if needed, or remove field - User: userResponse{ - ID: uuid.UUID(user.ID.Bytes).String(), - Email: user.Email, - Role: user.Role, - Ativo: user.Ativo, - Name: user.Nome, - Phone: user.Whatsapp, - CompanyID: companyID, - CompanyName: companyName, - AllowedRegions: user.RegioesPermitidas, - }, + // Prepare response + uResp := userResponse{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Role: user.Role, + Ativo: user.Ativo, // Added this back from original + Name: user.Nome, + CompanyID: companyID, + CompanyName: companyName, + AllowedRegions: user.RegioesPermitidas, + Phone: "", } + // Helper to get phone from profData or Client Data + if user.Role == "PHOTOGRAPHER" && profData != nil { + if profData.Whatsapp.Valid { + uResp.Phone = profData.Whatsapp.String + } + } else if user.Role == "EVENT_OWNER" { + // Fetch Client Data + clientData, err := h.service.GetClientData(c.Request.Context(), uResp.ID) + if err == nil && clientData != nil { + if clientData.Telefone.Valid { + uResp.Phone = clientData.Telefone.String + } + if clientData.CpfCnpj.Valid { + uResp.CpfCnpj = clientData.CpfCnpj.String + } + if clientData.Cep.Valid { + uResp.Cep = clientData.Cep.String + } + if clientData.Endereco.Valid { + uResp.Endereco = clientData.Endereco.String + } + if clientData.Numero.Valid { + uResp.Numero = clientData.Numero.String + } + if clientData.Complemento.Valid { + uResp.Complemento = clientData.Complemento.String + } + if clientData.Bairro.Valid { + uResp.Bairro = clientData.Bairro.String + } + if clientData.Cidade.Valid { + uResp.Cidade = clientData.Cidade.String + } + if clientData.Estado.Valid { + uResp.Estado = clientData.Estado.String + } + // Use client name if available ?? Or user name is fine. + // Usually user.Name comes from `usuarios` table, but `cadastro_clientes` also has nome. + // Let's stick to user.Name for consistency, usually they should be same. + } + } + + resp := loginResponse{ + AccessToken: tokenPair.AccessToken, + ExpiresAt: "2025-...", + User: uResp, + } if profData != nil { resp.Profissional = map[string]interface{}{ "id": uuid.UUID(profData.ID.Bytes).String(), @@ -368,38 +428,76 @@ func (h *Handler) Me(c *gin.Context) { allowedRegions = append(allowedRegions, user.RegioesPermitidas...) } + uResp := userResponse{ + ID: uuid.UUID(user.ID.Bytes).String(), + Email: user.Email, + Role: user.Role, + Ativo: user.Ativo, + Name: user.Nome, + Phone: user.Whatsapp, // Default to user.Whatsapp + CompanyName: empresaNome, + CompanyID: empresaID, + AllowedRegions: allowedRegions, + } + + if user.Role == "EVENT_OWNER" { + clientData, err := h.service.GetClientData(c.Request.Context(), uResp.ID) + if err == nil && clientData != nil { + if clientData.Telefone.Valid { + uResp.Phone = clientData.Telefone.String + } + if clientData.CpfCnpj.Valid { + uResp.CpfCnpj = clientData.CpfCnpj.String + } + if clientData.Cep.Valid { + uResp.Cep = clientData.Cep.String + } + if clientData.Endereco.Valid { + uResp.Endereco = clientData.Endereco.String + } + if clientData.Numero.Valid { + uResp.Numero = clientData.Numero.String + } + if clientData.Complemento.Valid { + uResp.Complemento = clientData.Complemento.String + } + if clientData.Bairro.Valid { + uResp.Bairro = clientData.Bairro.String + } + if clientData.Cidade.Valid { + uResp.Cidade = clientData.Cidade.String + } + if clientData.Estado.Valid { + uResp.Estado = clientData.Estado.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, - CompanyID: empresaID, - AllowedRegions: allowedRegions, - }, + User: uResp, } if user.Role == "PHOTOGRAPHER" || user.Role == "BUSINESS_OWNER" { regiao := c.GetString("regiao") - // If regiao is empty, we might skip fetching professional data or default? - // For now if empty, GetProfessionalByUserID with valid=true and string="" will likely fail or return empty? - // Queries check regiao = $2. If regiao is "", and DB has "SP", it won't match. - // So user needs to send header for Me to see pro data. - if regiao != "" { - profData, err := h.service.GetProfessionalByUserID(c.Request.Context(), uuid.UUID(user.ID.Bytes).String()) - if err == nil && profData != nil { - resp.Profissional = map[string]interface{}{ - "id": uuid.UUID(profData.ID.Bytes).String(), - "nome": profData.Nome, - "funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(), - "funcao_profissional": "", // Deprecated - "functions": profData.Functions, - "equipamentos": profData.Equipamentos.String, - "avatar_url": profData.AvatarUrl.String, - } + if regiao == "" { + regiao = c.GetHeader("x-regiao") + } + + profData, err := h.service.GetProfessionalByUserID(c.Request.Context(), uuid.UUID(user.ID.Bytes).String()) + if err == nil && profData != nil { + // Update phone from professional data if valid + if profData.Whatsapp.Valid { + resp.User.Phone = profData.Whatsapp.String + } + + resp.Profissional = map[string]interface{}{ + "id": uuid.UUID(profData.ID.Bytes).String(), + "nome": profData.Nome, + "funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(), + "funcao_profissional": "", // Deprecated + "functions": profData.Functions, + "equipamentos": profData.Equipamentos.String, + "avatar_url": profData.AvatarUrl.String, } } } @@ -407,6 +505,75 @@ func (h *Handler) Me(c *gin.Context) { c.JSON(http.StatusOK, resp) } +type updateMeRequest struct { + Nome string `json:"name"` + Telefone string `json:"phone"` + CpfCnpj string `json:"cpf_cnpj"` + Cep string `json:"cep"` + Endereco string `json:"endereco"` + Numero string `json:"numero"` + Complemento string `json:"complemento"` + Bairro string `json:"bairro"` + Cidade string `json:"cidade"` + Estado string `json:"estado"` +} + +// UpdateMe godoc +// @Summary Update current user profile +// @Description Update profile information (Name, Phone, Address). Currently for EVENT_OWNER. +// @Tags auth +// @Accept json +// @Produce json +// @Success 200 {object} map[string]string +// @Failure 400 {object} map[string]string +// @Failure 500 {object} map[string]string +// @Router /api/me [put] +func (h *Handler) UpdateMe(c *gin.Context) { + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + var req updateMeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload: " + err.Error()}) + return + } + + // For now, we assume this is mostly for Clients (EVENT_OWNER) + // based on the fields provided (address, cpf, etc). + // Future: Check role and call appropriate service if needed. + // But UpdateClientData uses "UpdateCadastroCliente" which is linked to user_id. + // If the user does not have a client record, it might fail or do nothing if query uses WHERE exists? + // The query uses UPDATE, so if no row, no update. + + // We should probably check role or just try to update. + // But to be safe, let's just call UpdateClientData. + // If the user is a Photographer, they should use the /profissionais/me PUT (if exists) or similar. + // But wait, the user complaint is about Clients. + + err := h.service.UpdateClientData(c.Request.Context(), userID.(string), UpdateClientInput{ + Nome: req.Nome, + Telefone: req.Telefone, + CpfCnpj: req.CpfCnpj, + Cep: req.Cep, + Endereco: req.Endereco, + Numero: req.Numero, + Complemento: req.Complemento, + Bairro: req.Bairro, + Cidade: req.Cidade, + Estado: req.Estado, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update profile: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "profile updated successfully"}) +} + // ListPending godoc // @Summary List pending users // @Description List users with ativo=false @@ -525,7 +692,7 @@ func (h *Handler) AdminCreateUser(c *gin.Context) { // Let's assume header is present or default. regiao = "SP" // Default for now if missing? Or error? } - user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.TipoProfissional, true, regiao) + user, err := h.service.AdminCreateUser(c.Request.Context(), req.Email, req.Senha, req.Role, req.Nome, req.TipoProfissional, true, regiao, req.EmpresaID, req.Telefone, req.CpfCnpj, req.Cep, req.Endereco, req.Numero, req.Complemento, req.Bairro, req.Cidade, req.Estado) if err != nil { if strings.Contains(err.Error(), "duplicate key") { c.JSON(http.StatusConflict, gin.H{"error": "email already registered"}) @@ -620,7 +787,11 @@ func (h *Handler) DeleteUser(c *gin.Context) { // @Security BearerAuth // @Router /api/admin/users [get] func (h *Handler) ListUsers(c *gin.Context) { - users, err := h.service.ListUsers(c.Request.Context()) + regiao := c.GetString("regiao") + if regiao == "" { + regiao = c.GetHeader("x-regiao") + } + users, err := h.service.ListUsers(c.Request.Context(), regiao) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 95c6ce5..6ad64b6 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -13,6 +13,7 @@ import ( "photum-backend/internal/profissionais" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" ) @@ -46,7 +47,7 @@ func NewService(queries *generated.Queries, profissionaisService *profissionais. } } -func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone, tipoProfissional string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput, regiao string) (*generated.Usuario, error) { +func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefone, tipoProfissional string, empresaID *string, profissionalData *profissionais.CreateProfissionalInput, regiao, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado string) (*generated.Usuario, error) { // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost) if err != nil { @@ -93,21 +94,27 @@ func (s *Service) Register(ctx context.Context, email, senha, role, nome, telefo // 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 + empID = pgtype.UUID{Bytes: parsedEmpID, 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 != ""}, + _, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{ + UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, + EmpresaID: empID, + Nome: pgtype.Text{String: nome, Valid: true}, + Telefone: pgtype.Text{String: telefone, Valid: telefone != ""}, + CpfCnpj: toPgText(&cpfCnpj), + Cep: toPgText(&cep), + Endereco: toPgText(&endereco), + Numero: toPgText(&numero), + Complemento: toPgText(&complemento), + Bairro: toPgText(&bairro), + Cidade: toPgText(&cidade), + Estado: toPgText(&estado), }) if err != nil { _ = s.queries.DeleteUsuario(ctx, user.ID) @@ -218,7 +225,7 @@ func (s *Service) ApproveUser(ctx context.Context, id string) error { return err } -func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, tipoProfissional string, ativo bool, regiao string) (*generated.Usuario, error) { +func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, tipoProfissional string, ativo bool, regiao string, empresaID string, telefone, cpfCnpj, cep, endereco, numero, complemento, bairro, cidade, estado string) (*generated.Usuario, error) { // Hash password hashedPassword, err := bcrypt.GenerateFromPassword([]byte(senha), bcrypt.DefaultCost) if err != nil { @@ -246,14 +253,10 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, _ = s.queries.DeleteUsuario(ctx, user.ID) return nil, err } - // Update the user struct in memory too (important for the return value?) - // user.RegioesPermitidas = []string{regiao} // Generated struct might differ, but return value is pointer to user. - // Since we return &user, and user is local struct from CreateUsuario, it has empty RegioesPermitidas. - // It's better to manually updating it if downstream depends on it, but usually ID/Email is enough. } if ativo { - // Approve user immediately (already active=true by default in DB? No, default false) + // Approve user immediately err = s.ApproveUser(ctx, uuid.UUID(user.ID.Bytes).String()) if err != nil { _ = s.queries.DeleteUsuario(ctx, user.ID) @@ -262,6 +265,33 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, user.Ativo = true } + // Link Company if EVENT_OWNER + if role == RoleEventOwner && empresaID != "" { + empUuid, err := uuid.Parse(empresaID) + if err == nil { + _, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{ + UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, + EmpresaID: pgtype.UUID{Bytes: empUuid, Valid: true}, + Nome: pgtype.Text{String: nome, Valid: true}, + Telefone: toPgText(&telefone), + CpfCnpj: toPgText(&cpfCnpj), + Cep: toPgText(&cep), + Endereco: toPgText(&endereco), + Numero: toPgText(&numero), + Complemento: toPgText(&complemento), + Bairro: toPgText(&bairro), + Cidade: toPgText(&cidade), + Estado: toPgText(&estado), + }) + if err != nil { + // Log error but maybe don't fail user creation? + // Ideally rollback + _ = s.queries.DeleteUsuario(ctx, user.ID) + return nil, err + } + } + } + // Create professional profile if applicable if role == RolePhotographer || role == RoleBusinessOwner { var funcaoID string @@ -285,22 +315,11 @@ func (s *Service) AdminCreateUser(ctx context.Context, email, senha, role, nome, Nome: nome, Email: &email, FuncaoProfissionalID: funcaoID, - // Add default whatsapp if user has one? User struct doesn't have phone here passed in args, - // but we can pass it if we update signature or just leave empty. } // Create the professional _, err = s.profissionaisService.Create(ctx, uuid.UUID(user.ID.Bytes).String(), profInput, regiao) if err != nil { - // Log error but don't fail user creation? - // Better to log. Backend logs not setup here. - // Just continue for now, or return error? - // If we fail here, user exists but has no profile. - // Ideally we should delete user or return error. - // Let's log and ignore for now to avoid breaking legacy flows if any. - // Actually, if this fails, the bug persists. Best to return error. - // But since we already committed user, we should probably return error so client knows. - // Try to delete user to rollback _ = s.queries.DeleteUsuario(ctx, user.ID) return nil, err @@ -367,20 +386,25 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error { if err != nil { fmt.Printf("[DEBUG] Creating User %s\n", u.Email) - // User not found (or error), try to create - // Note: We use "SP" as default regiao for professional creation - user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true, "SP") - if err != nil { - fmt.Printf("[DEBUG] Error creating user %s: %v\n", u.Email, err) - return err - } - // Update to include specific regions - err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{ - ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, - RegioesPermitidas: regions, - }) - if err != nil { - fmt.Printf("[DEBUG] Error updating regions for new user %s: %v\n", u.Email, err) + // Create if not exists + if errors.Is(err, pgx.ErrNoRows) { // Only create if user not found + fmt.Printf("Creating demo user: %s (%s)\n", u.Email, u.Role) + // Pass empty strings for new client fields for demo users + user, err := s.AdminCreateUser(ctx, u.Email, "123456", u.Role, u.Name, "", true, regions[0], "", "", "", "", "", "", "", "", "", "") + if err != nil { + fmt.Printf("Error creating demo user %s: %v\n", u.Email, err) + return err + } + // Update to include specific regions + err = s.queries.UpdateUsuarioRegions(ctx, generated.UpdateUsuarioRegionsParams{ + ID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true}, + RegioesPermitidas: regions, + }) + if err != nil { + fmt.Printf("[DEBUG] Error updating regions for new user %s: %v\n", u.Email, err) + return err + } + } else { // If it's another error, return it return err } } else { @@ -422,8 +446,46 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error { return nil } -func (s *Service) ListUsers(ctx context.Context) ([]generated.ListAllUsuariosRow, error) { - return s.queries.ListAllUsuarios(ctx) +type UpdateClientInput struct { + Nome string + Telefone string + CpfCnpj string + Cep string + Endereco string + Numero string + Complemento string + Bairro string + Cidade string + Estado string +} + +func (s *Service) UpdateClientData(ctx context.Context, userID string, input UpdateClientInput) error { + uid, err := uuid.Parse(userID) + if err != nil { + return err + } + + // Prepare params + params := generated.UpdateCadastroClienteParams{ + UsuarioID: pgtype.UUID{Bytes: uid, Valid: true}, + Nome: pgtype.Text{String: input.Nome, Valid: input.Nome != ""}, + Telefone: pgtype.Text{String: input.Telefone, Valid: input.Telefone != ""}, + CpfCnpj: pgtype.Text{String: input.CpfCnpj, Valid: input.CpfCnpj != ""}, + Cep: pgtype.Text{String: input.Cep, Valid: input.Cep != ""}, + Endereco: pgtype.Text{String: input.Endereco, Valid: input.Endereco != ""}, + Numero: pgtype.Text{String: input.Numero, Valid: input.Numero != ""}, + Complemento: pgtype.Text{String: input.Complemento, Valid: input.Complemento != ""}, + Bairro: pgtype.Text{String: input.Bairro, Valid: input.Bairro != ""}, + Cidade: pgtype.Text{String: input.Cidade, Valid: input.Cidade != ""}, + Estado: pgtype.Text{String: input.Estado, Valid: input.Estado != ""}, + } + + _, err = s.queries.UpdateCadastroCliente(ctx, params) + return err +} + +func (s *Service) ListUsers(ctx context.Context, regiao string) ([]generated.ListAllUsuariosRow, error) { + return s.queries.ListAllUsuarios(ctx, regiao) } func (s *Service) GetUser(ctx context.Context, id string) (*generated.GetUsuarioByIDRow, error) { @@ -458,6 +520,22 @@ func (s *Service) GetProfessionalByUserID(ctx context.Context, userID string) (* return &p, nil } +func (s *Service) GetClientData(ctx context.Context, userID string) (*generated.CadastroCliente, error) { + parsedUUID, err := uuid.Parse(userID) + if err != nil { + return nil, err + } + var pgID pgtype.UUID + pgID.Bytes = parsedUUID + pgID.Valid = true + + c, err := s.queries.GetCadastroClienteByUsuarioID(ctx, pgID) + if err != nil { + return nil, err + } + return &c, nil +} + func toPgText(s *string) pgtype.Text { if s == nil { return pgtype.Text{Valid: false} diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index 290fb86..ffe9e0d 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -79,6 +79,14 @@ type CadastroCliente struct { Telefone pgtype.Text `json:"telefone"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` + CpfCnpj pgtype.Text `json:"cpf_cnpj"` + Cep pgtype.Text `json:"cep"` + Endereco pgtype.Text `json:"endereco"` + Numero pgtype.Text `json:"numero"` + Complemento pgtype.Text `json:"complemento"` + Bairro pgtype.Text `json:"bairro"` + Cidade pgtype.Text `json:"cidade"` + Estado pgtype.Text `json:"estado"` } type CadastroFot struct { diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go index 6de8768..7259c14 100644 --- a/backend/internal/db/generated/usuarios.sql.go +++ b/backend/internal/db/generated/usuarios.sql.go @@ -12,16 +12,37 @@ import ( ) 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 +INSERT INTO cadastro_clientes ( + usuario_id, + empresa_id, + nome, + telefone, + cpf_cnpj, + cep, + endereco, + numero, + complemento, + bairro, + cidade, + estado +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) +RETURNING id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em, cpf_cnpj, cep, endereco, numero, complemento, bairro, cidade, estado ` 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"` + UsuarioID pgtype.UUID `json:"usuario_id"` + EmpresaID pgtype.UUID `json:"empresa_id"` + Nome pgtype.Text `json:"nome"` + Telefone pgtype.Text `json:"telefone"` + CpfCnpj pgtype.Text `json:"cpf_cnpj"` + Cep pgtype.Text `json:"cep"` + Endereco pgtype.Text `json:"endereco"` + Numero pgtype.Text `json:"numero"` + Complemento pgtype.Text `json:"complemento"` + Bairro pgtype.Text `json:"bairro"` + Cidade pgtype.Text `json:"cidade"` + Estado pgtype.Text `json:"estado"` } func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroClienteParams) (CadastroCliente, error) { @@ -30,6 +51,14 @@ func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroC arg.EmpresaID, arg.Nome, arg.Telefone, + arg.CpfCnpj, + arg.Cep, + arg.Endereco, + arg.Numero, + arg.Complemento, + arg.Bairro, + arg.Cidade, + arg.Estado, ) var i CadastroCliente err := row.Scan( @@ -40,6 +69,14 @@ func (q *Queries) CreateCadastroCliente(ctx context.Context, arg CreateCadastroC &i.Telefone, &i.CriadoEm, &i.AtualizadoEm, + &i.CpfCnpj, + &i.Cep, + &i.Endereco, + &i.Numero, + &i.Complemento, + &i.Bairro, + &i.Cidade, + &i.Estado, ) return i, err } @@ -91,6 +128,34 @@ func (q *Queries) DeleteUsuario(ctx context.Context, id pgtype.UUID) error { return err } +const getCadastroClienteByUsuarioID = `-- name: GetCadastroClienteByUsuarioID :one +SELECT id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em, cpf_cnpj, cep, endereco, numero, complemento, bairro, cidade, estado FROM cadastro_clientes +WHERE usuario_id = $1 LIMIT 1 +` + +func (q *Queries) GetCadastroClienteByUsuarioID(ctx context.Context, usuarioID pgtype.UUID) (CadastroCliente, error) { + row := q.db.QueryRow(ctx, getCadastroClienteByUsuarioID, usuarioID) + var i CadastroCliente + err := row.Scan( + &i.ID, + &i.UsuarioID, + &i.EmpresaID, + &i.Nome, + &i.Telefone, + &i.CriadoEm, + &i.AtualizadoEm, + &i.CpfCnpj, + &i.Cep, + &i.Endereco, + &i.Numero, + &i.Complemento, + &i.Bairro, + &i.Cidade, + &i.Estado, + ) + return i, err +} + const getUsuarioByEmail = `-- name: GetUsuarioByEmail :one SELECT u.id, u.email, u.senha_hash, u.role, u.tipo_profissional, u.ativo, u.criado_em, u.atualizado_em, u.regioes_permitidas, COALESCE(cp.nome, cc.nome, '') as nome, @@ -201,6 +266,7 @@ 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 $1::text = ANY(u.regioes_permitidas) ORDER BY u.criado_em DESC ` @@ -219,8 +285,8 @@ type ListAllUsuariosRow struct { EmpresaNome pgtype.Text `json:"empresa_nome"` } -func (q *Queries) ListAllUsuarios(ctx context.Context) ([]ListAllUsuariosRow, error) { - rows, err := q.db.Query(ctx, listAllUsuarios) +func (q *Queries) ListAllUsuarios(ctx context.Context, dollar_1 string) ([]ListAllUsuariosRow, error) { + rows, err := q.db.Query(ctx, listAllUsuarios, dollar_1) if err != nil { return nil, err } @@ -313,6 +379,73 @@ func (q *Queries) ListUsuariosPending(ctx context.Context, dollar_1 string) ([]L return items, nil } +const updateCadastroCliente = `-- name: UpdateCadastroCliente :one +UPDATE cadastro_clientes +SET + nome = COALESCE($2, nome), + telefone = COALESCE($3, telefone), + cpf_cnpj = COALESCE($4, cpf_cnpj), + cep = COALESCE($5, cep), + endereco = COALESCE($6, endereco), + numero = COALESCE($7, numero), + complemento = COALESCE($8, complemento), + bairro = COALESCE($9, bairro), + cidade = COALESCE($10, cidade), + estado = COALESCE($11, estado), + atualizado_em = NOW() +WHERE usuario_id = $1 +RETURNING id, usuario_id, empresa_id, nome, telefone, criado_em, atualizado_em, cpf_cnpj, cep, endereco, numero, complemento, bairro, cidade, estado +` + +type UpdateCadastroClienteParams struct { + UsuarioID pgtype.UUID `json:"usuario_id"` + Nome pgtype.Text `json:"nome"` + Telefone pgtype.Text `json:"telefone"` + CpfCnpj pgtype.Text `json:"cpf_cnpj"` + Cep pgtype.Text `json:"cep"` + Endereco pgtype.Text `json:"endereco"` + Numero pgtype.Text `json:"numero"` + Complemento pgtype.Text `json:"complemento"` + Bairro pgtype.Text `json:"bairro"` + Cidade pgtype.Text `json:"cidade"` + Estado pgtype.Text `json:"estado"` +} + +func (q *Queries) UpdateCadastroCliente(ctx context.Context, arg UpdateCadastroClienteParams) (CadastroCliente, error) { + row := q.db.QueryRow(ctx, updateCadastroCliente, + arg.UsuarioID, + arg.Nome, + arg.Telefone, + arg.CpfCnpj, + arg.Cep, + arg.Endereco, + arg.Numero, + arg.Complemento, + arg.Bairro, + arg.Cidade, + arg.Estado, + ) + var i CadastroCliente + err := row.Scan( + &i.ID, + &i.UsuarioID, + &i.EmpresaID, + &i.Nome, + &i.Telefone, + &i.CriadoEm, + &i.AtualizadoEm, + &i.CpfCnpj, + &i.Cep, + &i.Endereco, + &i.Numero, + &i.Complemento, + &i.Bairro, + &i.Cidade, + &i.Estado, + ) + return i, err +} + const updateUsuarioAtivo = `-- name: UpdateUsuarioAtivo :one UPDATE usuarios SET ativo = $2, atualizado_em = NOW() diff --git a/backend/internal/db/migrations/017_add_client_fields.up.sql b/backend/internal/db/migrations/017_add_client_fields.up.sql new file mode 100644 index 0000000..c716eb2 --- /dev/null +++ b/backend/internal/db/migrations/017_add_client_fields.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE cadastro_clientes +ADD COLUMN IF NOT EXISTS cpf_cnpj VARCHAR(20), +ADD COLUMN IF NOT EXISTS cep VARCHAR(10), +ADD COLUMN IF NOT EXISTS endereco VARCHAR(255), +ADD COLUMN IF NOT EXISTS numero VARCHAR(20), +ADD COLUMN IF NOT EXISTS complemento VARCHAR(100), +ADD COLUMN IF NOT EXISTS bairro VARCHAR(100), +ADD COLUMN IF NOT EXISTS cidade VARCHAR(100), +ADD COLUMN IF NOT EXISTS estado CHAR(2); diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql index 9f24758..6d097eb 100644 --- a/backend/internal/db/queries/usuarios.sql +++ b/backend/internal/db/queries/usuarios.sql @@ -77,9 +77,44 @@ 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 $1::text = ANY(u.regioes_permitidas) ORDER BY u.criado_em DESC; -- name: CreateCadastroCliente :one -INSERT INTO cadastro_clientes (usuario_id, empresa_id, nome, telefone) -VALUES ($1, $2, $3, $4) +INSERT INTO cadastro_clientes ( + usuario_id, + empresa_id, + nome, + telefone, + cpf_cnpj, + cep, + endereco, + numero, + complemento, + bairro, + cidade, + estado +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING *; + +-- name: UpdateCadastroCliente :one +UPDATE cadastro_clientes +SET + nome = COALESCE($2, nome), + telefone = COALESCE($3, telefone), + cpf_cnpj = COALESCE($4, cpf_cnpj), + cep = COALESCE($5, cep), + endereco = COALESCE($6, endereco), + numero = COALESCE($7, numero), + complemento = COALESCE($8, complemento), + bairro = COALESCE($9, bairro), + cidade = COALESCE($10, cidade), + estado = COALESCE($11, estado), + atualizado_em = NOW() +WHERE usuario_id = $1 +RETURNING *; + +-- name: GetCadastroClienteByUsuarioID :one +SELECT * FROM cadastro_clientes +WHERE usuario_id = $1 LIMIT 1; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 2aea773..d600bb0 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -201,6 +201,16 @@ CREATE TABLE IF NOT EXISTS cadastro_clientes ( atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(usuario_id) ); + +-- Migrations (Appended for Idempotency) +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS cpf_cnpj VARCHAR(20); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS cep VARCHAR(10); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS endereco VARCHAR(255); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS numero VARCHAR(20); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS complemento VARCHAR(100); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS bairro VARCHAR(100); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS cidade VARCHAR(100); +ALTER TABLE cadastro_clientes ADD COLUMN IF NOT EXISTS estado CHAR(2); -- Agenda Table CREATE TABLE IF NOT EXISTS agenda ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 4e613f4..c1ec97d 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -102,12 +102,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => id: backendUser.id, email: backendUser.email, name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0], + phone: backendUser.phone || backendUser.telefone || backendUser.whatsapp, // Map phone role: backendUser.role as UserRole, ativo: backendUser.ativo, empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId, companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName, avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar, allowedRegions: backendUser.allowed_regions || [], // Map allowed regions + + // Client specific fields + cpf_cnpj: backendUser.cpf_cnpj, + cep: backendUser.cep, + endereco: backendUser.endereco, + numero: backendUser.numero, + complemento: backendUser.complemento, + bairro: backendUser.bairro, + cidade: backendUser.cidade, + estado: backendUser.estado, }; console.log("AuthContext: restoreSession mapped user:", mappedUser); if (!backendUser.ativo) { @@ -191,12 +202,23 @@ const login = async (email: string, password?: string) => { id: backendUser.id, email: backendUser.email, name: data.profissional?.nome || data.empresa?.nome || backendUser.name || backendUser.nome || backendUser.email.split('@')[0], + phone: backendUser.phone || backendUser.telefone || backendUser.whatsapp, // Map phone role: backendUser.role as UserRole, ativo: backendUser.ativo, empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId, companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName, avatar: data.profissional?.avatar_url || data.empresa?.avatar_url || backendUser.avatar, allowedRegions: backendUser.allowed_regions || [], + + // Client specific fields + cpf_cnpj: backendUser.cpf_cnpj, + cep: backendUser.cep, + endereco: backendUser.endereco, + numero: backendUser.numero, + complemento: backendUser.complemento, + bairro: backendUser.bairro, + cidade: backendUser.cidade, + estado: backendUser.estado, }; setUser(mappedUser); @@ -240,21 +262,38 @@ const login = async (email: string, password?: string) => { } }; - const register = async (data: { nome: string; email: string; senha: string; telefone: string; role: string; empresaId?: string; tipo_profissional?: string; regiao?: string }) => { + const register = async (data: any) => { + setIsLoading(true); + try { - // Destructure to separate empresaId from the rest - const { empresaId, ...rest } = data; + const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; + const payload = { - ...rest, - empresa_id: empresaId + email: data.email, + senha: data.password || data.senha, + role: data.role, + nome: data.name || data.nome, + telefone: data.phone || data.telefone, + professional_type: data.professional_type || data.tipo_profissional, + empresa_id: data.company_id || data.empresaId || data.empresa_id, + regiao: data.regiao, + cpf_cnpj: data.cpf_cnpj, + cep: data.cep, + endereco: data.endereco, + numero: data.numero, + complemento: data.complemento, + bairro: data.bairro, + cidade: data.cidade, + estado: data.estado }; - const response = await fetch(`${import.meta.env.VITE_API_URL}/auth/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-regiao': data.regiao || 'SP' // Pass region header + + const response = await fetch(`${API_URL}/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-regiao": data.regiao || 'SP' }, - body: JSON.stringify(payload) + body: JSON.stringify(payload), }); if (!response.ok) { @@ -265,9 +304,6 @@ const login = async (email: string, password?: string) => { const responseData = await response.json(); - // IF user is returned (auto-login), logic: - // 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. if (responseData.user && responseData.user.ativo) { if (responseData.access_token) { localStorage.setItem('token', responseData.access_token); @@ -279,14 +315,26 @@ const login = async (email: string, password?: string) => { id: backendUser.id, email: backendUser.email, name: backendUser.nome || backendUser.email.split('@')[0], + phone: backendUser.phone || backendUser.telefone || backendUser.whatsapp, // Map phone role: backendUser.role as UserRole, ativo: backendUser.ativo, empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId, companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName, + avatar: backendUser.avatar, + allowedRegions: backendUser.allowed_regions || [], + + // Client specific fields + cpf_cnpj: backendUser.cpf_cnpj, + cep: backendUser.cep, + endereco: backendUser.endereco, + numero: backendUser.numero, + complemento: backendUser.complemento, + bairro: backendUser.bairro, + cidade: backendUser.cidade, + estado: backendUser.estado, }; setUser(mappedUser); } - // If user is NOT active, we do NOT set the token/user state, preventing auto-login. return { success: true, @@ -295,7 +343,12 @@ const login = async (email: string, password?: string) => { }; } catch (err) { console.error('Registration error:', err); - throw err; + if (err instanceof Error) { + return { success: false, error: err.message }; + } + return { success: false, error: "Erro desconhecido" }; + } finally { + setIsLoading(false); } }; diff --git a/frontend/pages/ProfessionalRegister.tsx b/frontend/pages/ProfessionalRegister.tsx index b85e955..13b2585 100644 --- a/frontend/pages/ProfessionalRegister.tsx +++ b/frontend/pages/ProfessionalRegister.tsx @@ -4,6 +4,7 @@ import { ProfessionalData, } from "../components/ProfessionalForm"; import { useAuth } from "../contexts/AuthContext"; +import { toast } from "react-hot-toast"; interface ProfessionalRegisterProps { onNavigate: (page: string) => void; @@ -59,7 +60,12 @@ export const ProfessionalRegister: React.FC = ({ console.log("Upload concluído. URL:", avatarUrl); } catch (err) { console.error("Erro no upload do avatar:", err); - throw new Error("Falha ao enviar foto de perfil: " + (err instanceof Error ? err.message : "Erro desconhecido")); + // Não trava o cadastro, apenas avisa + toast.error("Não foi possível enviar a foto de perfil. O cadastro seguirá sem foto.", { + duration: 5000, + icon: '⚠️' + }); + // Mantém avatarUrl vazio e prossegue } // Mapear dados do formulário para o payload esperado pelo backend diff --git a/frontend/pages/Profile.tsx b/frontend/pages/Profile.tsx index 79e0ec0..e10ca4c 100644 --- a/frontend/pages/Profile.tsx +++ b/frontend/pages/Profile.tsx @@ -6,7 +6,7 @@ import { import { Navbar } from "../components/Navbar"; import { Button } from "../components/Button"; import { useAuth } from "../contexts/AuthContext"; -import { getFunctions, createProfessional, updateProfessional } from "../services/apiService"; +import { getFunctions, createProfessional, updateProfessional, updateUserProfile } from "../services/apiService"; import { toast } from "react-hot-toast"; import { formatCPFCNPJ, formatPhone } from "../utils/masks"; @@ -127,6 +127,27 @@ export const ProfilePage: React.FC = () => { const funcsRes = await getFunctions(); if (funcsRes.data) setFunctions(funcsRes.data); + if (user?.role === "EVENT_OWNER") { + // Clients don't have professional profile. Populate from User. + // Ensure user object has these fields (mapped in AuthContext) + setFormData({ + ...formData, + nome: user.name || "", + email: user.email || "", + whatsapp: user.phone || "", + cpf_cnpj_titular: user.cpf_cnpj || "", + cep: user.cep || "", + endereco: user.endereco || "", + numero: user.numero || "", + complemento: user.complemento || "", + bairro: user.bairro || "", + cidade: user.cidade || "", + uf: user.estado || "", + }); + setIsLoading(false); + return; + } + // Try to fetch existing profile const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/me`, { headers: { Authorization: `Bearer ${token}` } @@ -270,6 +291,26 @@ export const ProfilePage: React.FC = () => { setIsSaving(true); try { if (!token) throw new Error("Usuário não autenticado"); + + if (user?.role === "EVENT_OWNER") { + const clientPayload = { + name: formData.nome, + phone: formData.whatsapp, + cpf_cnpj: formData.cpf_cnpj_titular, + cep: formData.cep, + endereco: formData.endereco, + numero: formData.numero, + complemento: formData.complemento, + bairro: formData.bairro, + cidade: formData.cidade, + estado: formData.uf + }; + const res = await updateUserProfile(clientPayload, token); + if (res.error) throw new Error(res.error); + toast.success("Perfil atualizado com sucesso!"); + setIsSaving(false); + return; + } // Payload preparation // For create/update, we need `funcao_profissional_id` (single) for backward compatibility optionally diff --git a/frontend/pages/Register.tsx b/frontend/pages/Register.tsx index b89a79b..63c8466 100644 --- a/frontend/pages/Register.tsx +++ b/frontend/pages/Register.tsx @@ -3,6 +3,7 @@ import { Button } from "../components/Button"; import { Input } from "../components/Input"; import { useAuth } from "../contexts/AuthContext"; import { getCompanies } from "../services/apiService"; +import { formatPhone, formatCPFCNPJ, formatCEP } from "../utils/masks"; interface RegisterProps { onNavigate: (page: string) => void; @@ -21,6 +22,14 @@ export const Register: React.FC = ({ onNavigate }) => { password: "", confirmPassword: "", empresaId: "", + cpfCnpj: "", + cep: "", + endereco: "", + numero: "", + complemento: "", + bairro: "", + cidade: "", + estado: "", }); const [agreedToTerms, setAgreedToTerms] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -84,12 +93,20 @@ export const Register: React.FC = ({ onNavigate }) => { try { await register({ - nome: formData.name, + name: formData.name, email: formData.email, - senha: formData.password, - telefone: formData.phone, + password: formData.password, + phone: formData.phone, role: "EVENT_OWNER", // Client Role - empresaId: formData.empresaId, + company_id: formData.empresaId, + cpf_cnpj: formData.cpfCnpj, + cep: formData.cep, + endereco: formData.endereco, + numero: formData.numero, + complemento: formData.complemento, + bairro: formData.bairro, + cidade: formData.cidade, + estado: formData.estado, }); // Limpar dados de sessão após cadastro bem-sucedido sessionStorage.removeItem('accessCodeValidated'); @@ -233,9 +250,82 @@ export const Register: React.FC = ({ onNavigate }) => { required placeholder="(00) 00000-0000" value={formData.phone} - onChange={(e) => handleChange("phone", e.target.value)} + onChange={(e) => handleChange("phone", formatPhone(e.target.value))} mask="phone" /> + + handleChange("cpfCnpj", formatCPFCNPJ(e.target.value))} + placeholder="000.000.000-00" + maxLength={18} + /> + +
+ handleChange("cep", formatCEP(e.target.value))} + placeholder="00000-000" + onBlur={(e) => { + const cep = e.target.value.replace(/\D/g, ''); + if (cep.length === 8) { + fetch(`https://viacep.com.br/ws/${cep}/json/`) + .then(res => res.json()) + .then(data => { + if (!data.erro) { + setFormData(prev => ({ + ...prev, + endereco: data.logradouro, + bairro: data.bairro, + cidade: data.localidade, + estado: data.uf + })); + } + }); + } + }} + /> + handleChange("numero", e.target.value)} + /> +
+ + handleChange("endereco", e.target.value)} + /> + +
+ handleChange("complemento", e.target.value)} + /> + handleChange("bairro", e.target.value)} + /> +
+ +
+ handleChange("cidade", e.target.value)} + /> + handleChange("estado", e.target.value)} + maxLength={2} + /> +