464 lines
16 KiB
Go
464 lines
16 KiB
Go
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"`
|
|
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),
|
|
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),
|
|
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),
|
|
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:
|
|
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),
|
|
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
|
|
}
|
|
|
|
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
|
|
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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
prof, err := h.service.GetByUserID(c.Request.Context(), userIDStr)
|
|
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
|
|
}
|
|
|
|
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"})
|
|
}
|