feat: habilita edição de perfil para clientes e corrige carga de dados

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.
This commit is contained in:
NANDO9322 2026-02-09 00:56:09 -03:00
parent 788e0dca70
commit 9c6ee3afdb
16 changed files with 981 additions and 168 deletions

View file

@ -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")
{

View file

@ -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{
// Prepare response
uResp := userResponse{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Role: user.Role,
Ativo: user.Ativo,
Ativo: user.Ativo, // Added this back from original
Name: user.Nome,
Phone: user.Whatsapp,
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,29 +428,68 @@ func (h *Handler) Me(c *gin.Context) {
allowedRegions = append(allowedRegions, user.RegioesPermitidas...)
}
resp := loginResponse{
User: userResponse{
uResp := userResponse{
ID: uuid.UUID(user.ID.Bytes).String(),
Email: user.Email,
Role: user.Role,
Ativo: user.Ativo,
Name: user.Nome,
Phone: user.Whatsapp,
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: 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 != "" {
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,
@ -402,11 +501,79 @@ 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

View file

@ -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,
_, err = s.queries.CreateCadastroCliente(ctx, generated.CreateCadastroClienteParams{
UsuarioID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
EmpresaID: empID,
Nome: pgtype.Text{String: nome, Valid: nome != ""},
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,11 +386,13 @@ 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")
// 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("[DEBUG] Error creating user %s: %v\n", u.Email, err)
fmt.Printf("Error creating demo user %s: %v\n", u.Email, err)
return err
}
// Update to include specific regions
@ -383,6 +404,9 @@ func (s *Service) EnsureDemoUsers(ctx context.Context) error {
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 {
// User exists, FORCE update password and ensure regions
fmt.Printf("[DEBUG] Updating Existing User %s\n", u.Email)
@ -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}

View file

@ -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 {

View file

@ -12,9 +12,22 @@ 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 {
@ -22,6 +35,14 @@ type CreateCadastroClienteParams struct {
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()

View file

@ -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);

View file

@ -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;

View file

@ -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(),

View file

@ -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',
const response = await fetch(`${API_URL}/auth/register`, {
method: "POST",
headers: {
'Content-Type': 'application/json',
'x-regiao': data.regiao || 'SP' // Pass region header
"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);
}
};

View file

@ -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<ProfessionalRegisterProps> = ({
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

View file

@ -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}` }
@ -271,6 +292,26 @@ export const ProfilePage: React.FC = () => {
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
// But we primarily use `funcoes_ids`.

View file

@ -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<RegisterProps> = ({ 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<RegisterProps> = ({ 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,10 +250,83 @@ export const Register: React.FC<RegisterProps> = ({ 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"
/>
<Input
label="CPF/CNPJ"
value={formData.cpfCnpj}
onChange={(e) => handleChange("cpfCnpj", formatCPFCNPJ(e.target.value))}
placeholder="000.000.000-00"
maxLength={18}
/>
<div className="grid grid-cols-2 gap-3">
<Input
label="CEP"
value={formData.cep}
onChange={(e) => 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
}));
}
});
}
}}
/>
<Input
label="Número"
value={formData.numero}
onChange={(e) => handleChange("numero", e.target.value)}
/>
</div>
<Input
label="Endereço"
value={formData.endereco}
onChange={(e) => handleChange("endereco", e.target.value)}
/>
<div className="grid grid-cols-2 gap-3">
<Input
label="Complemento"
value={formData.complemento}
onChange={(e) => handleChange("complemento", e.target.value)}
/>
<Input
label="Bairro"
value={formData.bairro}
onChange={(e) => handleChange("bairro", e.target.value)}
/>
</div>
<div className="grid grid-cols-[2fr_1fr] gap-3">
<Input
label="Cidade"
value={formData.cidade}
onChange={(e) => handleChange("cidade", e.target.value)}
/>
<Input
label="UF"
value={formData.estado}
onChange={(e) => handleChange("estado", e.target.value)}
maxLength={2}
/>
</div>
<div>
<label className="block text-[10px] sm:text-xs md:text-sm font-medium text-gray-700 mb-1">
Empresa *

View file

@ -7,6 +7,7 @@ import {
approveUser as apiApproveUser,
rejectUser as apiRejectUser,
updateUserRole,
getCompanies,
} from "../services/apiService";
import { UserApprovalStatus, UserRole } from "../types";
import {
@ -21,6 +22,7 @@ import {
} from "lucide-react";
import { Button } from "../components/Button";
import { Input } from "../components/Input";
import { formatPhone, formatCPFCNPJ, formatCEP } from "../utils/masks";
// INTERFACES
interface UserApprovalProps {
@ -194,6 +196,16 @@ const CreateUserModal: React.FC<CreateUserModalProps> = ({
formData,
setFormData
}) => {
// Fetch companies
const [companies, setCompanies] = useState<any[]>([]);
useEffect(() => {
if (formData.role === "EVENT_OWNER") {
getCompanies().then(res => {
if(res.data) setCompanies(res.data);
});
}
}, [formData.role]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4 fade-in">
@ -226,7 +238,7 @@ const CreateUserModal: React.FC<CreateUserModalProps> = ({
<Input
label="Telefone (Whatsapp)"
value={formData.telefone}
onChange={(e) => setFormData({...formData, telefone: e.target.value})}
onChange={(e) => setFormData({...formData, telefone: formatPhone(e.target.value)})}
/>
<div className="grid grid-cols-2 gap-4">
<Input
@ -237,40 +249,136 @@ const CreateUserModal: React.FC<CreateUserModalProps> = ({
value={formData.senha}
onChange={(e) => setFormData({...formData, senha: e.target.value})}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Função
Tipo de Usuário
</label>
<select
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
value={formData.role}
onChange={(e) => setFormData({...formData, role: e.target.value as any})}
onChange={(e) => setFormData({...formData, role: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
>
<option value="PHOTOGRAPHER">Profissional</option>
<option value="RESEARCHER">Pesquisador</option>
<option value="BUSINESS_OWNER">Dono do Negócio</option>
<option value="SUPERADMIN">Super Admin</option>
<option value="PHOTOGRAPHER">Fotógrafo</option>
<option value="EVENT_OWNER">Cliente (Empresa)</option>
<option value="BUSINESS_OWNER">Dono de Negócio</option>
<option value="ADMIN">Administrador</option>
<option value="AGENDA_VIEWER">Visualizador de Agenda</option>
<option value="RESEARCHER">Pesquisador</option>
</select>
</div>
{formData.role === "EVENT_OWNER" && (
<div className="space-y-4 border-t pt-4 mt-2">
<h4 className="font-medium text-gray-900">Dados do Cliente</h4>
<div className="grid grid-cols-2 gap-4">
<Input
label="CPF/CNPJ"
value={formData.cpfCnpj || ''}
onChange={(e) => setFormData({...formData, cpfCnpj: formatCPFCNPJ(e.target.value)})}
placeholder="000.000.000-00"
maxLength={18}
/>
<Input
label="CEP"
value={formData.cep || ''}
onChange={(e) => setFormData({...formData, 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
}));
}
});
}
}}
/>
</div>
<div className="grid grid-cols-[2fr_1fr] gap-4">
<Input
label="Endereço"
value={formData.endereco || ''}
onChange={(e) => setFormData({...formData, endereco: e.target.value})}
/>
<Input
label="Número"
value={formData.numero || ''}
onChange={(e) => setFormData({...formData, numero: e.target.value})}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<Input
label="Complemento"
value={formData.complemento || ''}
onChange={(e) => setFormData({...formData, complemento: e.target.value})}
/>
<Input
label="Bairro"
value={formData.bairro || ''}
onChange={(e) => setFormData({...formData, bairro: e.target.value})}
/>
</div>
<div className="grid grid-cols-[2fr_1fr] gap-4">
<Input
label="Cidade"
value={formData.cidade || ''}
onChange={(e) => setFormData({...formData, cidade: e.target.value})}
/>
<Input
label="UF"
value={formData.estado || ''}
onChange={(e) => setFormData({...formData, estado: e.target.value})}
maxLength={2}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Empresa *
</label>
<select
required
value={formData.empresa_id || ''}
onChange={(e) => setFormData({...formData, empresa_id: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
>
<option value="">Selecione uma empresa</option>
{companies.map(c => (
<option key={c.id} value={c.id}>{c.nome}</option>
))}
</select>
</div>
</div>
)}
{formData.role === "PHOTOGRAPHER" && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de Profissional
</label>
<select
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
value={formData.professional_type}
onChange={(e) => setFormData({...formData, professional_type: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-photum-green focus:border-transparent outline-none transition-all"
>
<option value="">Selecione...</option>
<option value="Fotógrafo">Fotógrafo</option>
<option value="Cinegrafista">Cinegrafista</option>
<option value="Recepcionista">Recepcionista</option>
<option value="FOTOGRAFO">Fotógrafo</option>
<option value="CINEGRAFISTA">Cinegrafista</option>
<option value="EDITOR">Editor</option>
</select>
</div>
</div>
)}
<div className="pt-4 flex justify-end gap-3">
@ -316,6 +424,16 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
role: "PHOTOGRAPHER",
telefone: "",
professional_type: "", // For photographer subtype
empresa_id: "",
cpfCnpj: "",
cep: "",
endereco: "",
numero: "",
complemento: "",
bairro: "",
cidade: "",
estado: "",
regiao: "",
});
const fetchUsers = async () => {
@ -393,7 +511,17 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
senha: createFormData.senha,
role: createFormData.role,
telefone: createFormData.telefone,
tipo_profissional: createFormData.role === "PHOTOGRAPHER" ? createFormData.professional_type : undefined
professional_type: createFormData.role === "PHOTOGRAPHER" ? createFormData.professional_type : undefined,
empresa_id: createFormData.role === "EVENT_OWNER" ? createFormData.empresa_id : undefined,
cpf_cnpj: createFormData.role === "EVENT_OWNER" ? createFormData.cpfCnpj : undefined,
cep: createFormData.role === "EVENT_OWNER" ? createFormData.cep : undefined,
endereco: createFormData.role === "EVENT_OWNER" ? createFormData.endereco : undefined,
numero: createFormData.role === "EVENT_OWNER" ? createFormData.numero : undefined,
complemento: createFormData.role === "EVENT_OWNER" ? createFormData.complemento : undefined,
bairro: createFormData.role === "EVENT_OWNER" ? createFormData.bairro : undefined,
cidade: createFormData.role === "EVENT_OWNER" ? createFormData.cidade : undefined,
estado: createFormData.role === "EVENT_OWNER" ? createFormData.estado : undefined,
regiao: createFormData.regiao || undefined
};
const result = await createAdminUser(payload, token);
@ -403,7 +531,8 @@ export const UserApproval: React.FC<UserApprovalProps> = ({ onNavigate }) => {
alert("Usuário criado com sucesso!");
setShowCreateModal(false);
setCreateFormData({
nome: "", email: "", senha: "", role: "PHOTOGRAPHER", telefone: "", professional_type: ""
nome: "", email: "", senha: "", role: "PHOTOGRAPHER", telefone: "", professional_type: "", empresa_id: "",
cpfCnpj: "", cep: "", endereco: "", numero: "", complemento: "", bairro: "", cidade: "", estado: "", regiao: ""
});
fetchUsers();
}

View file

@ -159,6 +159,42 @@ export async function updateProfessional(id: string, data: any, token: string):
}
}
/**
* Atualiza o perfil do usuário logado (Cliente/Evento Owner)
*/
export async function updateUserProfile(data: any, token: string): Promise<ApiResponse<any>> {
try {
const response = await fetch(`${API_BASE_URL}/api/me`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`,
"x-regiao": localStorage.getItem("photum_selected_region") || "SP"
},
body: JSON.stringify(data),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const responseData = await response.json();
return {
data: responseData,
error: null,
isBackendDown: false,
};
} catch (error) {
console.error("Error updating user profile:", error);
return {
data: null,
error: error instanceof Error ? error.message : "Erro desconhecido",
isBackendDown: true,
};
}
}
/**
* Remove um profissional
*/

View file

@ -48,7 +48,18 @@ export interface User {
ativo?: boolean;
empresaId?: string; // ID da empresa vinculada (para Business Owners)
companyName?: string; // Nome da empresa vinculada
allowedRegions?: string[]; // Regiões permitidas
allowedRegions?: string[]; // Regiões permitidas para o usuário
// Client / Event Owner specific fields
cpf_cnpj?: string;
cep?: string;
endereco?: string;
numero?: string;
complemento?: string;
bairro?: string;
cidade?: string;
estado?: string;
}
export interface Institution {

View file

@ -1,24 +1,26 @@
export const formatCPFCNPJ = (value: string) => {
const clean = value.replace(/\D/g, "");
if (clean.length <= 11) {
// CPF
return clean
const digits = value.replace(/\D/g, "");
if (digits.length <= 11) {
return digits
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})/, "$1-$2")
.replace(/(-\d{2})\d+?$/, "$1");
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
} else {
// CNPJ
return clean
return digits
.replace(/^(\d{2})(\d)/, "$1.$2")
.replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
.replace(/\.(\d{3})(\d)/, ".$1/$2")
.replace(/(\d{4})(\d)/, "$1-$2")
.replace(/(-\d{2})\d+?$/, "$1");
.replace(/(\d{4})(\d)/, "$1-$2");
}
};
export const formatCEP = (value: string) => {
return value
.replace(/\D/g, "")
.replace(/^(\d{5})(\d)/, "$1-$2")
.slice(0, 9);
};
export const formatPhone = (value: string) => {
const clean = value.replace(/\D/g, "");