package profissionais import ( "encoding/json" "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"` // Deprecated single name (optional) FuncaoProfissionalID string `json:"funcao_profissional_id"` Functions json.RawMessage `json:"functions"` // JSON array 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"` Conta *string `json:"conta"` 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: "", Functions: json.RawMessage("[]"), // Empty on Create (or Fetch specifically if needed) 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), Conta: fromPgText(v.Conta), 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 removed from query or changed? Functions: toJSONRaw(v.Functions), 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), Conta: fromPgText(v.Conta), 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 removed Functions: toJSONRaw(v.Functions), 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), Conta: fromPgText(v.Conta), 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.GetProfissionalByUsuarioIDRow: 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: "", Functions: toJSONRaw(v.Functions), 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), Conta: fromPgText(v.Conta), 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), } 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 } func toJSONRaw(v interface{}) json.RawMessage { if v == nil { return json.RawMessage("[]") } switch val := v.(type) { case []byte: if len(val) == 0 { return json.RawMessage("[]") } return json.RawMessage(val) case string: if val == "" { return json.RawMessage("[]") } return json.RawMessage([]byte(val)) default: // Fallback: marshal strictly? or return empty array b, _ := json.Marshal(v) return json.RawMessage(b) } } // 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 // Also handle Region override userRole, _ := c.Get("role") roleStr, _ := userRole.(string) if input.TargetUserID != nil && *input.TargetUserID != "" { if roleStr != "SUPERADMIN" && roleStr != "BUSINESS_OWNER" { input.TargetUserID = nil } } regiao := c.GetString("regiao") // If input has regiao and user is admin, use it if input.Regiao != nil && *input.Regiao != "" { if roleStr == "SUPERADMIN" || roleStr == "BUSINESS_OWNER" { regiao = *input.Regiao } } prof, err := h.service.Create(c.Request.Context(), userIDStr, input, regiao) 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) { regiao := c.GetString("regiao") profs, err := h.service.List(c.Request.Context(), regiao) 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) } // Me godoc // @Summary Get current authenticated profissional // @Description Get the profissional profile associated with the current user // @Tags profissionais // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} ProfissionalResponse // @Failure 401 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/profissionais/me [get] func (h *Handler) Me(c *gin.Context) { 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"}) return } regiao := c.GetString("regiao") prof, err := h.service.GetByUserID(c.Request.Context(), userIDStr, regiao) if err != nil { // Checks if error is "no rows in result set" -> 404 if err.Error() == "no rows in result set" { c.JSON(http.StatusNotFound, gin.H{"error": "professional profile not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Reuse toResponse which handles different types via interface{} check // We need to add case for GetProfissionalByUsuarioIDRow in toResponse function c.JSON(http.StatusOK, toResponse(*prof)) } // 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 } regiao := c.GetString("regiao") prof, err := h.service.GetByID(c.Request.Context(), id, regiao) 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 } regiao := c.GetString("regiao") prof, err := h.service.Update(c.Request.Context(), id, input, regiao) 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 } regiao := c.GetString("regiao") err := h.service.Delete(c.Request.Context(), id, regiao) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // Import godoc // @Summary Import professionals from properties // @Description Import professionals (Upsert by CPF) // @Tags profissionais // @Accept json // @Produce json // @Security BearerAuth // @Param request body []CreateProfissionalInput true "List of Professionals" // @Success 200 {object} ImportStats // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/profissionais/import [post] func (h *Handler) Import(c *gin.Context) { var input []CreateProfissionalInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Role check: Only ADMIN or OWNER userRole, exists := c.Get("role") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } roleStr := userRole.(string) if roleStr != "SUPERADMIN" && roleStr != "BUSINESS_OWNER" { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } regiao := c.GetString("regiao") stats, errs := h.service.Import(c.Request.Context(), input, regiao) // Construct response with errors if any response := gin.H{ "created": stats.Created, "updated": stats.Updated, "errors_count": stats.Errors, } if len(errs) > 0 { var errMsgs []string for _, e := range errs { errMsgs = append(errMsgs, e.Error()) } response["errors"] = errMsgs } c.JSON(http.StatusOK, response) } // CheckCPF godoc // @Summary Check if professional exists by CPF // @Description Check existence and claim status // @Tags profissionais // @Accept json // @Produce json // @Param cpf query string true "CPF/CNPJ" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Router /api/public/profissionais/check [get] func (h *Handler) CheckCPF(c *gin.Context) { cpf := c.Query("cpf") if cpf == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "cpf required"}) return } // Clean CPF (optional, but good practice) // For now rely on exact match or client cleaning. // Ideally service should clean. // Check existence regiao := c.GetString("regiao") exists, claimed, name, err := h.service.CheckExistence(c.Request.Context(), cpf, regiao) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Return limited info for public check c.JSON(http.StatusOK, gin.H{ "exists": exists, "claimed": claimed, "name": name, // Returning name might be PII, but usually acceptable for confirmation "Is this you?" }) }