photum/backend/cmd/api/main.go
NANDO9322 f8bb2e66dd feat: suporte completo multi-região (SP/MG) e melhorias na validação de importação
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.
2026-02-05 16:18:40 -03:00

300 lines
12 KiB
Go

package main
import (
"context"
"log"
"photum-backend/docs"
"photum-backend/internal/agenda"
"photum-backend/internal/anos_formaturas"
"photum-backend/internal/auth"
"photum-backend/internal/availability"
"photum-backend/internal/cadastro_fot"
"photum-backend/internal/codigos"
"photum-backend/internal/config"
"photum-backend/internal/cursos"
"photum-backend/internal/db"
"photum-backend/internal/empresas"
"photum-backend/internal/escalas"
"photum-backend/internal/finance"
"photum-backend/internal/funcoes"
"photum-backend/internal/notification"
"photum-backend/internal/logistica"
"photum-backend/internal/profissionais"
"photum-backend/internal/storage"
"photum-backend/internal/tipos_eventos"
"photum-backend/internal/tipos_servicos"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
// "photum-backend/docs" is already imported above
)
// @title Photum Backend API
// @version 1.0
// @description Backend authentication service for Photum.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /
// @tag.name auth
// @tag.description Authentication related operations
// @tag.name admin
// @tag.description Administration operations
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
func main() {
cfg := config.LoadConfig()
log.Printf("Loaded DSN: %s", cfg.DBDsn)
queries, pool := db.Connect(cfg)
defer pool.Close()
// Run Migrations
db.Migrate(pool)
// Initialize services
// Initialize services
notificationService := notification.NewService()
profissionaisService := profissionais.NewService(queries)
authService := auth.NewService(queries, profissionaisService, cfg)
funcoesService := funcoes.NewService(queries)
cursosService := cursos.NewService(queries)
empresasService := empresas.NewService(queries)
anosFormaturasService := anos_formaturas.NewService(queries)
tiposServicosService := tipos_servicos.NewService(queries)
tiposEventosService := tipos_eventos.NewService(queries)
cadastroFotService := cadastro_fot.NewService(queries)
agendaService := agenda.NewService(queries, notificationService, cfg)
availabilityService := availability.NewService(queries)
s3Service := storage.NewS3Service(cfg)
// Seed Demo Users
if err := authService.EnsureDemoUsers(context.Background()); err != nil {
log.Printf("Failed to seed demo users: %v", err)
}
// Initialize handlers
authHandler := auth.NewHandler(authService, s3Service)
profissionaisHandler := profissionais.NewHandler(profissionaisService)
funcoesHandler := funcoes.NewHandler(funcoesService)
cursosHandler := cursos.NewHandler(cursosService)
empresasHandler := empresas.NewHandler(empresasService)
anosFormaturasHandler := anos_formaturas.NewHandler(anosFormaturasService)
tiposServicosHandler := tipos_servicos.NewHandler(tiposServicosService)
tiposEventosHandler := tipos_eventos.NewHandler(tiposEventosService)
cadastroFotHandler := cadastro_fot.NewHandler(cadastroFotService)
agendaHandler := agenda.NewHandler(agendaService)
availabilityHandler := availability.NewHandler(availabilityService)
escalasHandler := escalas.NewHandler(escalas.NewService(queries))
logisticaHandler := logistica.NewHandler(logistica.NewService(queries, notificationService, cfg))
codigosHandler := codigos.NewHandler(codigos.NewService(queries))
financeHandler := finance.NewHandler(finance.NewService(queries, profissionaisService))
r := gin.Default()
// CORS Middleware
configCors := cors.DefaultConfig()
if cfg.CorsAllowedOrigins == "*" {
configCors.AllowAllOrigins = true
} else {
configCors.AllowOrigins = strings.Split(cfg.CorsAllowedOrigins, ",")
}
configCors.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization", "x-regiao"}
r.Use(cors.New(configCors))
// Swagger
// Dynamically update Swagger Info
docs.SwaggerInfo.Host = cfg.SwaggerHost
if cfg.AppEnv == "production" {
docs.SwaggerInfo.Schemes = []string{"https", "http"}
} else {
docs.SwaggerInfo.Schemes = []string{"http", "https"}
}
// Swagger UI - usando URL relativa para funcionar em qualquer ambiente
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler,
ginSwagger.PersistAuthorization(true),
ginSwagger.DeepLinking(true),
ginSwagger.URL("doc.json"),
))
// Public Routes
authGroup := r.Group("/auth")
{
authGroup.POST("/register", authHandler.Register)
authGroup.POST("/login", authHandler.Login)
authGroup.POST("/refresh", authHandler.Refresh)
authGroup.POST("/logout", authHandler.Logout)
authGroup.POST("/upload-url", authHandler.GetUploadURL)
}
// Public API Routes (Data Lists) - Need Region Context
// Simple middleware to extract x-regiao header for public routes
publicWithRegion := r.Group("/api")
publicWithRegion.Use(func(c *gin.Context) {
regiao := c.GetHeader("x-regiao")
if regiao == "" {
regiao = "SP" // Default to SP for public access if not specified
}
c.Set("regiao", regiao)
c.Next()
})
{
publicWithRegion.GET("/funcoes", funcoesHandler.List)
publicWithRegion.GET("/cursos", cursosHandler.List)
publicWithRegion.GET("/empresas", empresasHandler.List)
publicWithRegion.GET("/anos-formaturas", anosFormaturasHandler.List)
publicWithRegion.GET("/profissionais/check", profissionaisHandler.CheckCPF) // Public Check
publicWithRegion.GET("/tipos-servicos", tiposServicosHandler.List)
publicWithRegion.GET("/tipos-eventos", tiposEventosHandler.List)
publicWithRegion.GET("/tipos-eventos/:id/precos", tiposEventosHandler.ListPrices)
publicWithRegion.GET("/public/codigos-acesso/verificar", codigosHandler.Verify) // already has explicit handler but consistent context helps
}
// Protected Routes
api := r.Group("/api")
api.Use(auth.AuthMiddleware(cfg))
{
api.GET("/me", authHandler.Me)
profGroup := api.Group("/profissionais")
{
profGroup.POST("", profissionaisHandler.Create)
profGroup.POST("/import", profissionaisHandler.Import)
profGroup.GET("", profissionaisHandler.List)
profGroup.GET("/me", profissionaisHandler.Me)
profGroup.GET("/:id", profissionaisHandler.Get)
profGroup.PUT("/:id", profissionaisHandler.Update)
profGroup.DELETE("/:id", profissionaisHandler.Delete)
// Rota de extrato financeiro (usando agendaHandler por conveniência de serviço)
profGroup.GET("/me/financial-statement", agendaHandler.GetProfessionalFinancialStatement)
}
funcoesGroup := api.Group("/funcoes")
{
funcoesGroup.POST("", funcoesHandler.Create)
funcoesGroup.PUT("/:id", funcoesHandler.Update)
funcoesGroup.DELETE("/:id", funcoesHandler.Delete)
}
// protected CRUD (create/update/delete)
api.POST("/cursos", cursosHandler.Create)
api.PUT("/cursos/:id", cursosHandler.Update)
api.DELETE("/cursos/:id", cursosHandler.Delete)
api.POST("/empresas", empresasHandler.Create)
api.PUT("/empresas/:id", empresasHandler.Update)
api.DELETE("/empresas/:id", empresasHandler.Delete)
api.POST("/anos-formaturas", anosFormaturasHandler.Create)
api.PUT("/anos-formaturas/:id", anosFormaturasHandler.Update)
api.DELETE("/anos-formaturas/:id", anosFormaturasHandler.Delete)
api.POST("/tipos-servicos", tiposServicosHandler.Create)
api.PUT("/tipos-servicos/:id", tiposServicosHandler.Update)
api.DELETE("/tipos-servicos/:id", tiposServicosHandler.Delete)
api.POST("/tipos-eventos", tiposEventosHandler.Create)
api.PUT("/tipos-eventos/:id", tiposEventosHandler.Update)
api.DELETE("/tipos-eventos/:id", tiposEventosHandler.Delete)
api.POST("/tipos-eventos/precos", tiposEventosHandler.SetPrice)
api.GET("/cadastro-fot", cadastroFotHandler.List)
api.POST("/cadastro-fot", cadastroFotHandler.Create)
api.GET("/cadastro-fot/:id", cadastroFotHandler.Get)
api.PUT("/cadastro-fot/:id", cadastroFotHandler.Update)
api.DELETE("/cadastro-fot/:id", cadastroFotHandler.Delete)
api.POST("/import/fot", cadastroFotHandler.Import)
// Agenda routes - read access for AGENDA_VIEWER
api.GET("/agenda", agendaHandler.List)
api.GET("/agenda/:id", agendaHandler.Get)
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals)
// Agenda routes - write access (blocked for AGENDA_VIEWER)
api.POST("/agenda", auth.RequireWriteAccess(), agendaHandler.Create)
api.PUT("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Update)
api.DELETE("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Delete)
api.POST("/agenda/:id/professionals", auth.RequireWriteAccess(), agendaHandler.AssignProfessional)
api.DELETE("/agenda/:id/professionals/:profId", auth.RequireWriteAccess(), agendaHandler.RemoveProfessional)
api.PATCH("/agenda/:id/professionals/:profId/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus)
api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition)
api.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus)
api.POST("/agenda/:id/notify-logistics", auth.RequireWriteAccess(), agendaHandler.NotifyLogistics)
api.POST("/import/agenda", auth.RequireWriteAccess(), agendaHandler.Import)
api.POST("/availability", availabilityHandler.SetAvailability)
api.GET("/availability", availabilityHandler.ListAvailability)
// Escalas Routes
api.POST("/escalas", escalasHandler.Create)
api.GET("/escalas", escalasHandler.ListByAgenda)
api.DELETE("/escalas/:id", escalasHandler.Delete)
api.PUT("/escalas/:id", escalasHandler.Update)
// Logistics Routes - blocked for AGENDA_VIEWER
logisticaGroup := api.Group("/logistica", auth.RequireLogisticsAccess())
{
logisticaGroup.POST("/carros", logisticaHandler.CreateCarro)
logisticaGroup.GET("/carros", logisticaHandler.ListCarros)
logisticaGroup.DELETE("/carros/:id", logisticaHandler.DeleteCarro)
logisticaGroup.PUT("/carros/:id", logisticaHandler.UpdateCarro)
logisticaGroup.POST("/carros/:id/passageiros", logisticaHandler.AddPassenger)
logisticaGroup.DELETE("/carros/:id/passageiros/:profID", logisticaHandler.RemovePassenger)
logisticaGroup.GET("/carros/:id/passageiros", logisticaHandler.ListPassengers)
}
codigosGroup := api.Group("/codigos-acesso")
{
codigosGroup.POST("", codigosHandler.Create)
codigosGroup.GET("", codigosHandler.List)
codigosGroup.DELETE("/:id", codigosHandler.Delete)
}
financeGroup := api.Group("/finance")
{
financeGroup.POST("", financeHandler.Create)
financeGroup.POST("/import", financeHandler.Import)
financeGroup.GET("", financeHandler.List)
financeGroup.GET("/autofill", financeHandler.AutoFill)
financeGroup.GET("/fot-events", financeHandler.GetFotEvents)
financeGroup.GET("/fot-search", financeHandler.SearchFot)
financeGroup.GET("/professionals", financeHandler.SearchProfessionals)
financeGroup.GET("/price", financeHandler.GetPrice)
financeGroup.PUT("/:id", financeHandler.Update)
financeGroup.DELETE("/:id", financeHandler.Delete)
}
admin := api.Group("/admin")
{
admin.GET("/users", authHandler.ListUsers)
admin.GET("/users/pending", authHandler.ListPending)
admin.GET("/users/:id", authHandler.GetUser)
admin.PATCH("/users/:id/approve", authHandler.Approve)
admin.POST("/users", authHandler.AdminCreateUser)
admin.PATCH("/users/:id/role", authHandler.UpdateRole)
admin.DELETE("/users/:id", authHandler.DeleteUser)
}
}
log.Printf("Swagger Host Configured: %s", cfg.SwaggerHost)
log.Printf("Server running on port %s", cfg.AppPort)
r.Run(":" + cfg.AppPort)
}