Detalhes das alterações: [Banco de Dados] - Ajuste nas constraints UNIQUE das tabelas de catálogo (cursos, empresas, tipos_eventos, etc.) para incluir a coluna `regiao`, permitindo dados duplicados entre regiões mas únicos por região. - Correção crítica na constraint da tabela `precos_tipos_eventos` para evitar conflitos de UPSERT (ON CONFLICT) durante a inicialização. - Implementação de lógica de Seed para a região 'MG': - Clonagem automática de catálogos base de 'SP' para 'MG' (Tipos de Evento, Serviços, etc.). - Inserção de tabela de preços específica para 'MG' via script de migração. [Backend - Go] - Atualização geral dos Handlers e Services para filtrar dados baseados no cabeçalho `x-regiao`. - Ajuste no Middleware de autenticação para processar e repassar o contexto da região. - Correção de queries SQL (geradas pelo sqlc) para suportar os novos filtros regionais. [Frontend - React] - Implementação do envio global do cabeçalho `x-regiao` nas requisições da API. - Correção no componente [PriceTableEditor](cci:1://file:///c:/Projetos/photum/frontend/components/System/PriceTableEditor.tsx:26:0-217:2) para carregar e salvar preços respeitando a região selecionada (fix de "Preços zerados" em MG). - Refatoração profunda na tela de Importação ([ImportData.tsx](cci:7://file:///c:/Projetos/photum/frontend/pages/ImportData.tsx:0:0-0:0)): - Adição de feedback visual detalhado para registros ignorados. - Categorização explícita de erros: "CPF Inválido", "Região Incompatível", "Linha Vazia/Separador". - Correção na lógica de contagem para considerar linhas vazias explicitamente no relatório final, garantindo que o total bata com o Excel. [Geral] - Correção de diversos erros de lint e tipagem TSX. - Padronização de logs de erro no backend para facilitar debug.
559 lines
19 KiB
Go
559 lines
19 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
|
|
}
|
|
}
|
|
}
|
|
|
|
regiao := c.GetString("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?"
|
|
})
|
|
}
|