photum/backend/internal/profissionais/handler.go
NANDO9322 7010e8e7d9 fix: correções de email, avatar e filtro de equipe no agendador
Backend:
- Corrigido mapeamento de email na listagem e busca por ID de profissionais. Agora o sistema utiliza o email do usuário vinculado (`usuario_email`) como fallback caso o email do perfil profissional esteja vazio.

Frontend:
- EventScheduler: Implementado filtro estrito para exibir apenas profissionais que foram explicitamente adicionados à equipe do evento ("Gerenciar Equipe"), prevenindo escalações indevidas.
- EventScheduler: Adicionada validação para ocultar cargos administrativos sem função operacional definida.
- ProfessionalDetailsModal: Corrigida a lógica de exibição do avatar para suportar a propriedade `avatar_url` (padrão atual do backend), resolvendo o problema de imagens quebradas ou ícones genéricos.
2026-01-12 12:20:08 -03:00

364 lines
12 KiB
Go

package profissionais
import (
"net/http"
"photum-backend/internal/db/generated"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
// ProfissionalResponse struct for Swagger and JSON response
type ProfissionalResponse struct {
ID string `json:"id"`
UsuarioID string `json:"usuario_id"`
Nome string `json:"nome"`
FuncaoProfissional string `json:"funcao_profissional"` // Now returns name from join
FuncaoProfissionalID string `json:"funcao_profissional_id"`
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
Whatsapp *string `json:"whatsapp"`
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"`
Agencia *string `json:"agencia"`
ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"`
QtdEstudio *int `json:"qtd_estudio"`
TipoCartao *string `json:"tipo_cartao"`
Observacao *string `json:"observacao"`
QualTec *int `json:"qual_tec"`
EducacaoSimpatia *int `json:"educacao_simpatia"`
DesempenhoEvento *int `json:"desempenho_evento"`
DispHorario *int `json:"disp_horario"`
Media *float64 `json:"media"`
TabelaFree *string `json:"tabela_free"`
ExtraPorEquipamento *bool `json:"extra_por_equipamento"`
Equipamentos *string `json:"equipamentos"`
Email *string `json:"email"`
AvatarURL *string `json:"avatar_url"`
}
func toResponse(p interface{}) ProfissionalResponse {
// Handle different types returned by queries (Create returns CadastroProfissionai, List/Get returns Row with join)
// This is a bit hacky, ideally we'd have a unified model or separate response mappers.
// For now, let's check type.
switch v := p.(type) {
case generated.CadastroProfissionai:
return ProfissionalResponse{
ID: uuid.UUID(v.ID.Bytes).String(),
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
Nome: v.Nome,
FuncaoProfissionalID: uuid.UUID(v.FuncaoProfissionalID.Bytes).String(),
// FuncaoProfissional name is not available in simple insert return without extra query or join
FuncaoProfissional: "",
Endereco: fromPgText(v.Endereco),
Cidade: fromPgText(v.Cidade),
Uf: fromPgText(v.Uf),
Whatsapp: fromPgText(v.Whatsapp),
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia),
ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio),
QtdEstudio: fromPgInt4(v.QtdEstudio),
TipoCartao: fromPgText(v.TipoCartao),
Observacao: fromPgText(v.Observacao),
QualTec: fromPgInt4(v.QualTec),
EducacaoSimpatia: fromPgInt4(v.EducacaoSimpatia),
DesempenhoEvento: fromPgInt4(v.DesempenhoEvento),
DispHorario: fromPgInt4(v.DispHorario),
Media: fromPgNumeric(v.Media),
TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos),
Email: fromPgText(v.Email),
AvatarURL: fromPgText(v.AvatarUrl),
}
case generated.ListProfissionaisRow:
email := fromPgText(v.Email)
if email == nil {
email = fromPgText(v.UsuarioEmail)
}
return ProfissionalResponse{
ID: uuid.UUID(v.ID.Bytes).String(),
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
Nome: v.Nome,
FuncaoProfissionalID: uuid.UUID(v.FuncaoProfissionalID.Bytes).String(),
FuncaoProfissional: v.FuncaoNome.String, // From join
Endereco: fromPgText(v.Endereco),
Cidade: fromPgText(v.Cidade),
Uf: fromPgText(v.Uf),
Whatsapp: fromPgText(v.Whatsapp),
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia),
ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio),
QtdEstudio: fromPgInt4(v.QtdEstudio),
TipoCartao: fromPgText(v.TipoCartao),
Observacao: fromPgText(v.Observacao),
QualTec: fromPgInt4(v.QualTec),
EducacaoSimpatia: fromPgInt4(v.EducacaoSimpatia),
DesempenhoEvento: fromPgInt4(v.DesempenhoEvento),
DispHorario: fromPgInt4(v.DispHorario),
Media: fromPgNumeric(v.Media),
TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos),
Email: email,
AvatarURL: fromPgText(v.AvatarUrl),
}
case generated.GetProfissionalByIDRow:
return ProfissionalResponse{
ID: uuid.UUID(v.ID.Bytes).String(),
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
Nome: v.Nome,
FuncaoProfissionalID: uuid.UUID(v.FuncaoProfissionalID.Bytes).String(),
FuncaoProfissional: v.FuncaoNome.String, // From join
Endereco: fromPgText(v.Endereco),
Cidade: fromPgText(v.Cidade),
Uf: fromPgText(v.Uf),
Whatsapp: fromPgText(v.Whatsapp),
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia),
ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio),
QtdEstudio: fromPgInt4(v.QtdEstudio),
TipoCartao: fromPgText(v.TipoCartao),
Observacao: fromPgText(v.Observacao),
QualTec: fromPgInt4(v.QualTec),
EducacaoSimpatia: fromPgInt4(v.EducacaoSimpatia),
DesempenhoEvento: fromPgInt4(v.DesempenhoEvento),
DispHorario: fromPgInt4(v.DispHorario),
Media: fromPgNumeric(v.Media),
TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos),
Email: fromPgText(v.Email),
AvatarURL: fromPgText(v.AvatarUrl),
}
default:
return ProfissionalResponse{}
}
}
// Helpers for conversion
func fromPgText(t pgtype.Text) *string {
if !t.Valid {
return nil
}
return &t.String
}
func fromPgBool(b pgtype.Bool) *bool {
if !b.Valid {
return nil
}
return &b.Bool
}
func fromPgInt4(i pgtype.Int4) *int {
if !i.Valid {
return nil
}
val := int(i.Int32)
return &val
}
func fromPgNumeric(n pgtype.Numeric) *float64 {
if !n.Valid {
return nil
}
f, _ := n.Float64Value()
val := f.Float64
return &val
}
// Create godoc
// @Summary Create a new profissional
// @Description Create a new profissional record
// @Tags profissionais
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreateProfissionalInput true "Create Profissional Request"
// @Success 201 {object} ProfissionalResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/profissionais [post]
func (h *Handler) Create(c *gin.Context) {
var input CreateProfissionalInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
return
}
userIDStr, ok := userID.(string)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid user id type in context"})
return
}
// Security: Only allow TargetUserID if user is ADMIN or OWNER
if input.TargetUserID != nil && *input.TargetUserID != "" {
userRole, exists := c.Get("role")
if !exists {
// Should validation fail? Or just ignore target?
// Safer to ignore target user ID if role not found
input.TargetUserID = nil
} else {
roleStr, ok := userRole.(string)
if !ok || (roleStr != "SUPERADMIN" && roleStr != "BUSINESS_OWNER") {
input.TargetUserID = nil
}
}
}
prof, err := h.service.Create(c.Request.Context(), userIDStr, input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, toResponse(*prof))
}
// List godoc
// @Summary List profissionais
// @Description List all profissionais
// @Tags profissionais
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {array} ProfissionalResponse
// @Failure 500 {object} map[string]string
// @Router /api/profissionais [get]
func (h *Handler) List(c *gin.Context) {
profs, err := h.service.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var response []ProfissionalResponse
for _, p := range profs {
response = append(response, toResponse(p))
}
c.JSON(http.StatusOK, response)
}
// Get godoc
// @Summary Get profissional by ID
// @Description Get a profissional by ID
// @Tags profissionais
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Profissional ID"
// @Success 200 {object} ProfissionalResponse
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/profissionais/{id} [get]
func (h *Handler) Get(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
prof, err := h.service.GetByID(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, toResponse(*prof))
}
// Update godoc
// @Summary Update profissional
// @Description Update a profissional by ID
// @Tags profissionais
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Profissional ID"
// @Param request body UpdateProfissionalInput true "Update Profissional Request"
// @Success 200 {object} ProfissionalResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/profissionais/{id} [put]
func (h *Handler) Update(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
var input UpdateProfissionalInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
prof, err := h.service.Update(c.Request.Context(), id, input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, toResponse(*prof))
}
// Delete godoc
// @Summary Delete profissional
// @Description Delete a profissional by ID
// @Tags profissionais
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Profissional ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /api/profissionais/{id} [delete]
func (h *Handler) Delete(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "id required"})
return
}
err := h.service.Delete(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}