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:
parent
788e0dca70
commit
9c6ee3afdb
16 changed files with 981 additions and 168 deletions
|
|
@ -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")
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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 *
|
||||
|
|
|
|||
|
|
@ -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,39 +249,135 @@ const CreateUserModal: React.FC<CreateUserModalProps> = ({
|
|||
value={formData.senha}
|
||||
onChange={(e) => setFormData({...formData, senha: e.target.value})}
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Função
|
||||
</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})}
|
||||
>
|
||||
<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="EVENT_OWNER">Cliente (Empresa)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo de Usuário
|
||||
</label>
|
||||
<select
|
||||
value={formData.role}
|
||||
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">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>
|
||||
<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})}
|
||||
>
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Fotógrafo">Fotógrafo</option>
|
||||
<option value="Cinegrafista">Cinegrafista</option>
|
||||
<option value="Recepcionista">Recepcionista</option>
|
||||
</select>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Tipo de Profissional
|
||||
</label>
|
||||
<select
|
||||
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="FOTOGRAFO">Fotógrafo</option>
|
||||
<option value="CINEGRAFISTA">Cinegrafista</option>
|
||||
<option value="EDITOR">Editor</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue