photum/backend/internal/finance/handler.go
NANDO9322 765496065a feat: suporte a FOT alfanumérico e correções de UI
Backend:
- Migration 007: alterada coluna `fot` de INTEGER para VARCHAR(50).
- Ajustados serviços (finance, agenda) e handlers para processar FOT como string.
- Regenerados modelos e queries do banco de dados (sqlc).

Frontend:
- [FotForm](cci:1://file:///c:/Projetos/photum/frontend/components/FotForm.tsx:13:0-348:2): Permitido input de texto/alfanumérico (ex: "20000MG").
- [EventTable](cci:1://file:///c:/Projetos/photum/frontend/components/EventTable.tsx:29:0-684:2): Removido bloqueio do botão "Aprovar" para equipes incompletas.
- [Dashboard](cci:1://file:///c:/Projetos/photum/frontend/pages/Dashboard.tsx:31:0-1749:2): Corrigida duplicação do campo "Qtd Formandos".
- [Dashboard](cci:1://file:///c:/Projetos/photum/frontend/pages/Dashboard.tsx:31:0-1749:2): Filtros de "Gerenciar Equipe" agora usam funções dinâmicas (IDs) em vez de valores fixos.
- `Navbar`: Logo agora redireciona corretamente para `/painel`.
2026-01-29 22:15:14 -03:00

293 lines
7.9 KiB
Go

package finance
import (
"fmt"
"net/http"
"photum-backend/internal/db/generated"
"time"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgtype"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
// Request DTO
type TransactionRequest struct {
FotID *string `json:"fot_id"`
DataCobranca string `json:"data_cobranca"` // YYYY-MM-DD
TipoEvento string `json:"tipo_evento"`
TipoServico string `json:"tipo_servico"`
ProfessionalName string `json:"professional_name"`
Whatsapp string `json:"whatsapp"`
Cpf string `json:"cpf"`
TabelaFree string `json:"tabela_free"`
ValorFree float64 `json:"valor_free"`
ValorExtra float64 `json:"valor_extra"`
DescricaoExtra string `json:"descricao_extra"`
DataPagamento *string `json:"data_pagamento"`
PgtoOk bool `json:"pgto_ok"`
}
// Helper: Parse Date String to pgtype.Date
func parseDate(dateStr string) pgtype.Date {
if dateStr == "" {
return pgtype.Date{Valid: false}
}
t, err := time.Parse("2006-01-02", dateStr)
if err != nil {
return pgtype.Date{Valid: false}
}
return pgtype.Date{Time: t, Valid: true}
}
// Helper: Float to Numeric
func floatToNumeric(f float64) pgtype.Numeric {
s := fmt.Sprintf("%.2f", f)
var n pgtype.Numeric
n.Scan(s)
return n
}
// Helper: String UUID to pgtype.UUID
func parseUUID(uuidStr *string) pgtype.UUID {
if uuidStr == nil || *uuidStr == "" {
return pgtype.UUID{Valid: false}
}
var u pgtype.UUID
err := u.Scan(*uuidStr)
if err != nil {
return pgtype.UUID{Valid: false}
}
return u
}
// Helper: String to pgtype.Text
func toText(s string) pgtype.Text {
return pgtype.Text{String: s, Valid: s != ""}
}
func (h *Handler) Create(c *gin.Context) {
var req TransactionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
params := generated.CreateTransactionParams{
FotID: parseUUID(req.FotID),
DataCobranca: parseDate(req.DataCobranca),
TipoEvento: toText(req.TipoEvento),
TipoServico: toText(req.TipoServico),
ProfessionalName: toText(req.ProfessionalName),
Whatsapp: toText(req.Whatsapp),
Cpf: toText(req.Cpf),
TabelaFree: toText(req.TabelaFree),
ValorFree: floatToNumeric(req.ValorFree),
ValorExtra: floatToNumeric(req.ValorExtra),
DescricaoExtra: toText(req.DescricaoExtra),
TotalPagar: floatToNumeric(req.ValorFree + req.ValorExtra),
PgtoOk: pgtype.Bool{Bool: req.PgtoOk, Valid: true},
}
if req.DataPagamento != nil {
params.DataPagamento = parseDate(*req.DataPagamento)
}
txn, err := h.service.Create(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, txn)
}
func (h *Handler) Update(c *gin.Context) {
idStr := c.Param("id")
var idUUID pgtype.UUID
if err := idUUID.Scan(idStr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
var req TransactionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
params := generated.UpdateTransactionParams{
ID: idUUID,
FotID: parseUUID(req.FotID),
DataCobranca: parseDate(req.DataCobranca),
TipoEvento: toText(req.TipoEvento),
TipoServico: toText(req.TipoServico),
ProfessionalName: toText(req.ProfessionalName),
Whatsapp: toText(req.Whatsapp),
Cpf: toText(req.Cpf),
TabelaFree: toText(req.TabelaFree),
ValorFree: floatToNumeric(req.ValorFree),
ValorExtra: floatToNumeric(req.ValorExtra),
DescricaoExtra: toText(req.DescricaoExtra),
TotalPagar: floatToNumeric(req.ValorFree + req.ValorExtra),
PgtoOk: pgtype.Bool{Bool: req.PgtoOk, Valid: true},
}
if req.DataPagamento != nil {
params.DataPagamento = parseDate(*req.DataPagamento)
}
txn, err := h.service.Update(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, txn)
}
func (h *Handler) Delete(c *gin.Context) {
idStr := c.Param("id")
var idUUID pgtype.UUID
if err := idUUID.Scan(idStr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
return
}
if err := h.service.Delete(c.Request.Context(), idUUID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
func (h *Handler) List(c *gin.Context) {
fotIDStr := c.Query("fot_id")
if fotIDStr != "" {
var fotUUID pgtype.UUID
if err := fotUUID.Scan(fotIDStr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Fot ID"})
return
}
list, err := h.service.ListByFot(c.Request.Context(), fotUUID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, list)
return
}
list, err := h.service.ListAll(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, list)
}
func (h *Handler) AutoFill(c *gin.Context) {
fotNumStr := c.Query("fot")
if fotNumStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "FOT Number required"})
return
}
fotData, err := h.service.AutoFillSearch(c.Request.Context(), fotNumStr)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "FOT not found"})
return
}
c.JSON(http.StatusOK, fotData)
}
func (h *Handler) GetFotEvents(c *gin.Context) {
fotNumStr := c.Query("fot_id") // Accepts UUID string currently, or we can look up by number if needed.
// User has UUID from AutoFill
var fotUUID pgtype.UUID
if err := fotUUID.Scan(fotNumStr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid FOT ID"})
return
}
events, err := h.service.ListFotEvents(c.Request.Context(), fotUUID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, events)
}
func (h *Handler) SearchProfessionals(c *gin.Context) {
query := c.Query("q") // can be empty if function is provided potentially? No, search usually needs text.
functionName := c.Query("function")
// If function is provided, we might want to list all if query is empty?
// For now let's enforce query length if function is missing, or allow empty query if function is present.
if query == "" && functionName == "" {
c.JSON(http.StatusOK, []interface{}{})
return
}
if functionName != "" {
pros, err := h.service.SearchProfessionalsByFunction(c.Request.Context(), query, functionName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, pros)
return
}
pros, err := h.service.SearchProfessionals(c.Request.Context(), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, pros)
}
func (h *Handler) GetPrice(c *gin.Context) {
eventName := c.Query("event")
serviceName := c.Query("service")
if eventName == "" || serviceName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing event or service parameters"})
return
}
price, err := h.service.GetStandardPrice(c.Request.Context(), eventName, serviceName)
if err != nil {
// likely not found, return 0 or 404
c.JSON(http.StatusOK, gin.H{"valor": 0})
return
}
val, _ := price.Float64Value()
c.JSON(http.StatusOK, gin.H{"valor": val.Float64})
}
func (h *Handler) SearchFot(c *gin.Context) {
query := c.Query("q")
if query == "" {
c.JSON(http.StatusOK, []interface{}{})
return
}
results, err := h.service.SearchFot(c.Request.Context(), query)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, results)
}