feat: implementação do financeiro e suporte a múltiplas funções

Este commit introduz o módulo financeiro completo e refatora o sistema de profissionais para suportar múltiplas funções, corrigindo a contabilização e validação de equipes.

Principais alterações:

- **Módulo Financeiro:**
  - Criação da tabela `financial_transactions` e queries associadas.
  - Implementação do backend (Handler/Service) para gerenciar transações.
  - Nova página [Finance.tsx](cci:7://file:///c:/Projetos/photum/frontend/pages/Finance.tsx:0:0-0:0) com listagem, edição, filtros avançados e agrupamento por FOT.
  - Correção na busca de FOTs e formatação de datas.

- **Gestão de Equipe e Profissionais:**
  - Refatoração para suportar múltiplas funções por profissional (Backend & Frontend).
  - Atualização do [Dashboard](cci:1://file:///c:/Projetos/photum/frontend/pages/Dashboard.tsx:31:0-1663:2) e [EventTable](cci:1://file:///c:/Projetos/photum/frontend/components/EventTable.tsx:28:0-659:2) para contabilizar corretamente profissionais (Fotografo, Cinegrafista, Recepcionista) verificando a lista de funções.
  - Implementação de validação de cota no aceite de convites (bloqueia se a equipe da função específica já estiver completa).
  - Ajuste visual nos indicadores de "Equipe Completa" e contadores de faltantes na listagem de eventos.

- **Geral:**
  - Atualização da documentação Swagger.
  - Ajustes de tipagem e migrações de banco de dados.
This commit is contained in:
NANDO9322 2026-01-15 18:07:39 -03:00
parent a1d5434414
commit e78de535c1
31 changed files with 2813 additions and 1070 deletions

View file

@ -16,7 +16,9 @@ import (
"photum-backend/internal/db"
"photum-backend/internal/empresas"
"photum-backend/internal/escalas"
"photum-backend/internal/finance"
"photum-backend/internal/funcoes"
"photum-backend/internal/logistica"
"photum-backend/internal/profissionais"
"photum-backend/internal/storage"
@ -98,6 +100,7 @@ func main() {
escalasHandler := escalas.NewHandler(escalas.NewService(queries))
logisticaHandler := logistica.NewHandler(logistica.NewService(queries))
codigosHandler := codigos.NewHandler(codigos.NewService(queries))
financeHandler := finance.NewHandler(finance.NewService(queries))
r := gin.Default()
@ -240,6 +243,19 @@ func main() {
codigosGroup.DELETE("/:id", codigosHandler.Delete)
}
financeGroup := api.Group("/finance")
{
financeGroup.POST("", financeHandler.Create)
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)

View file

@ -3333,6 +3333,13 @@ const docTemplate = `{
"funcao_profissional_id": {
"type": "string"
},
"funcoes_ids": {
"description": "New field",
"type": "array",
"items": {
"type": "string"
}
},
"media": {
"type": "number"
},
@ -3415,12 +3422,15 @@ const docTemplate = `{
"type": "boolean"
},
"funcao_profissional": {
"description": "Now returns name from join",
"description": "Deprecated single name (optional)",
"type": "string"
},
"funcao_profissional_id": {
"type": "string"
},
"functions": {
"description": "New JSON array"
},
"id": {
"type": "string"
},
@ -3507,6 +3517,13 @@ const docTemplate = `{
"funcao_profissional_id": {
"type": "string"
},
"funcoes_ids": {
"description": "New field",
"type": "array",
"items": {
"type": "string"
}
},
"media": {
"type": "number"
},

View file

@ -3327,6 +3327,13 @@
"funcao_profissional_id": {
"type": "string"
},
"funcoes_ids": {
"description": "New field",
"type": "array",
"items": {
"type": "string"
}
},
"media": {
"type": "number"
},
@ -3409,12 +3416,15 @@
"type": "boolean"
},
"funcao_profissional": {
"description": "Now returns name from join",
"description": "Deprecated single name (optional)",
"type": "string"
},
"funcao_profissional_id": {
"type": "string"
},
"functions": {
"description": "New JSON array"
},
"id": {
"type": "string"
},
@ -3501,6 +3511,13 @@
"funcao_profissional_id": {
"type": "string"
},
"funcoes_ids": {
"description": "New field",
"type": "array",
"items": {
"type": "string"
}
},
"media": {
"type": "number"
},

View file

@ -319,6 +319,11 @@ definitions:
type: boolean
funcao_profissional_id:
type: string
funcoes_ids:
description: New field
items:
type: string
type: array
media:
type: number
nome:
@ -374,10 +379,12 @@ definitions:
extra_por_equipamento:
type: boolean
funcao_profissional:
description: Now returns name from join
description: Deprecated single name (optional)
type: string
funcao_profissional_id:
type: string
functions:
description: New JSON array
id:
type: string
media:
@ -435,6 +442,11 @@ definitions:
type: boolean
funcao_profissional_id:
type: string
funcoes_ids:
description: New field
items:
type: string
type: array
media:
type: number
nome:

View file

@ -242,7 +242,8 @@ func (h *Handler) Login(c *gin.Context) {
"id": uuid.UUID(profData.ID.Bytes).String(),
"nome": profData.Nome,
"funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(),
"funcao_profissional": profData.FuncaoNome.String,
"funcao_profissional": "", // Deprecated/Removed from query
"functions": profData.Functions,
"equipamentos": profData.Equipamentos.String,
"avatar_url": profData.AvatarUrl.String,
}
@ -369,7 +370,8 @@ func (h *Handler) Me(c *gin.Context) {
"id": uuid.UUID(profData.ID.Bytes).String(),
"nome": profData.Nome,
"funcao_profissional_id": uuid.UUID(profData.FuncaoProfissionalID.Bytes).String(),
"funcao_profissional": profData.FuncaoNome.String,
"funcao_profissional": "", // Deprecated
"functions": profData.Functions,
"equipamentos": profData.Equipamentos.String,
"avatar_url": profData.AvatarUrl.String,
}

View file

@ -462,6 +462,98 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) {
return items, nil
}
const listAgendasByFot = `-- name: ListAgendasByFot :many
SELECT
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status,
te.nome as tipo_evento_nome
FROM agenda a
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
WHERE a.fot_id = $1
ORDER BY a.data_evento
`
type ListAgendasByFotRow struct {
ID pgtype.UUID `json:"id"`
UserID pgtype.UUID `json:"user_id"`
FotID pgtype.UUID `json:"fot_id"`
DataEvento pgtype.Date `json:"data_evento"`
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
LocalEvento pgtype.Text `json:"local_evento"`
Endereco pgtype.Text `json:"endereco"`
Horario pgtype.Text `json:"horario"`
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
StatusProfissionais pgtype.Text `json:"status_profissionais"`
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
CineFaltante pgtype.Int4 `json:"cine_faltante"`
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
PreVenda pgtype.Bool `json:"pre_venda"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Status pgtype.Text `json:"status"`
TipoEventoNome string `json:"tipo_evento_nome"`
}
func (q *Queries) ListAgendasByFot(ctx context.Context, fotID pgtype.UUID) ([]ListAgendasByFotRow, error) {
rows, err := q.db.Query(ctx, listAgendasByFot, fotID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAgendasByFotRow
for rows.Next() {
var i ListAgendasByFotRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.FotID,
&i.DataEvento,
&i.TipoEventoID,
&i.ObservacoesEvento,
&i.LocalEvento,
&i.Endereco,
&i.Horario,
&i.QtdFormandos,
&i.QtdFotografos,
&i.QtdRecepcionistas,
&i.QtdCinegrafistas,
&i.QtdEstudios,
&i.QtdPontoFoto,
&i.QtdPontoID,
&i.QtdPontoDecorado,
&i.QtdPontosLed,
&i.QtdPlataforma360,
&i.StatusProfissionais,
&i.FotoFaltante,
&i.RecepFaltante,
&i.CineFaltante,
&i.LogisticaObservacoes,
&i.PreVenda,
&i.CriadoEm,
&i.AtualizadoEm,
&i.Status,
&i.TipoEventoNome,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listAgendasByUser = `-- name: ListAgendasByUser :many
SELECT
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status,

View file

@ -98,6 +98,62 @@ func (q *Queries) GetCadastroFotByFOT(ctx context.Context, fot int32) (CadastroF
return i, err
}
const getCadastroFotByFotJoin = `-- name: GetCadastroFotByFotJoin :one
SELECT
c.id, c.fot, c.empresa_id, c.curso_id, c.ano_formatura_id, c.instituicao, c.cidade, c.estado, c.observacoes, c.gastos_captacao, c.pre_venda, c.created_at, c.updated_at,
e.nome as empresa_nome,
cur.nome as curso_nome,
a.ano_semestre as ano_formatura_label
FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.fot = $1
`
type GetCadastroFotByFotJoinRow struct {
ID pgtype.UUID `json:"id"`
Fot int32 `json:"fot"`
EmpresaID pgtype.UUID `json:"empresa_id"`
CursoID pgtype.UUID `json:"curso_id"`
AnoFormaturaID pgtype.UUID `json:"ano_formatura_id"`
Instituicao pgtype.Text `json:"instituicao"`
Cidade pgtype.Text `json:"cidade"`
Estado pgtype.Text `json:"estado"`
Observacoes pgtype.Text `json:"observacoes"`
GastosCaptacao pgtype.Numeric `json:"gastos_captacao"`
PreVenda pgtype.Bool `json:"pre_venda"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
EmpresaNome string `json:"empresa_nome"`
CursoNome string `json:"curso_nome"`
AnoFormaturaLabel string `json:"ano_formatura_label"`
}
func (q *Queries) GetCadastroFotByFotJoin(ctx context.Context, fot int32) (GetCadastroFotByFotJoinRow, error) {
row := q.db.QueryRow(ctx, getCadastroFotByFotJoin, fot)
var i GetCadastroFotByFotJoinRow
err := row.Scan(
&i.ID,
&i.Fot,
&i.EmpresaID,
&i.CursoID,
&i.AnoFormaturaID,
&i.Instituicao,
&i.Cidade,
&i.Estado,
&i.Observacoes,
&i.GastosCaptacao,
&i.PreVenda,
&i.CreatedAt,
&i.UpdatedAt,
&i.EmpresaNome,
&i.CursoNome,
&i.AnoFormaturaLabel,
)
return i, err
}
const getCadastroFotByID = `-- name: GetCadastroFotByID :one
SELECT
c.id, c.fot, c.empresa_id, c.curso_id, c.ano_formatura_id, c.instituicao, c.cidade, c.estado, c.observacoes, c.gastos_captacao, c.pre_venda, c.created_at, c.updated_at,
@ -293,6 +349,77 @@ func (q *Queries) ListCadastroFotByEmpresa(ctx context.Context, empresaID pgtype
return items, nil
}
const searchFot = `-- name: SearchFot :many
SELECT
c.id, c.fot, c.empresa_id, c.curso_id, c.ano_formatura_id, c.instituicao, c.cidade, c.estado, c.observacoes, c.gastos_captacao, c.pre_venda, c.created_at, c.updated_at,
e.nome as empresa_nome,
cur.nome as curso_nome,
a.ano_semestre as ano_formatura_label
FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%'
ORDER BY c.fot ASC
LIMIT 10
`
type SearchFotRow struct {
ID pgtype.UUID `json:"id"`
Fot int32 `json:"fot"`
EmpresaID pgtype.UUID `json:"empresa_id"`
CursoID pgtype.UUID `json:"curso_id"`
AnoFormaturaID pgtype.UUID `json:"ano_formatura_id"`
Instituicao pgtype.Text `json:"instituicao"`
Cidade pgtype.Text `json:"cidade"`
Estado pgtype.Text `json:"estado"`
Observacoes pgtype.Text `json:"observacoes"`
GastosCaptacao pgtype.Numeric `json:"gastos_captacao"`
PreVenda pgtype.Bool `json:"pre_venda"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
EmpresaNome string `json:"empresa_nome"`
CursoNome string `json:"curso_nome"`
AnoFormaturaLabel string `json:"ano_formatura_label"`
}
func (q *Queries) SearchFot(ctx context.Context, dollar_1 pgtype.Text) ([]SearchFotRow, error) {
rows, err := q.db.Query(ctx, searchFot, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchFotRow
for rows.Next() {
var i SearchFotRow
if err := rows.Scan(
&i.ID,
&i.Fot,
&i.EmpresaID,
&i.CursoID,
&i.AnoFormaturaID,
&i.Instituicao,
&i.Cidade,
&i.Estado,
&i.Observacoes,
&i.GastosCaptacao,
&i.PreVenda,
&i.CreatedAt,
&i.UpdatedAt,
&i.EmpresaNome,
&i.CursoNome,
&i.AnoFormaturaLabel,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateCadastroFot = `-- name: UpdateCadastroFot :one
UPDATE cadastro_fot SET
fot = $2,
@ -356,3 +483,20 @@ func (q *Queries) UpdateCadastroFot(ctx context.Context, arg UpdateCadastroFotPa
)
return i, err
}
const updateCadastroFotGastos = `-- name: UpdateCadastroFotGastos :exec
UPDATE cadastro_fot SET
gastos_captacao = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateCadastroFotGastosParams struct {
ID pgtype.UUID `json:"id"`
GastosCaptacao pgtype.Numeric `json:"gastos_captacao"`
}
func (q *Queries) UpdateCadastroFotGastos(ctx context.Context, arg UpdateCadastroFotGastosParams) error {
_, err := q.db.Exec(ctx, updateCadastroFotGastos, arg.ID, arg.GastosCaptacao)
return err
}

View file

@ -0,0 +1,311 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: financial_transactions.sql
package generated
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createTransaction = `-- name: CreateTransaction :one
INSERT INTO financial_transactions (
fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name,
whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra,
total_pagar, data_pagamento, pgto_ok
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
) RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em
`
type CreateTransactionParams struct {
FotID pgtype.UUID `json:"fot_id"`
DataCobranca pgtype.Date `json:"data_cobranca"`
TipoEvento pgtype.Text `json:"tipo_evento"`
TipoServico pgtype.Text `json:"tipo_servico"`
ProfessionalName pgtype.Text `json:"professional_name"`
Whatsapp pgtype.Text `json:"whatsapp"`
Cpf pgtype.Text `json:"cpf"`
TabelaFree pgtype.Text `json:"tabela_free"`
ValorFree pgtype.Numeric `json:"valor_free"`
ValorExtra pgtype.Numeric `json:"valor_extra"`
DescricaoExtra pgtype.Text `json:"descricao_extra"`
TotalPagar pgtype.Numeric `json:"total_pagar"`
DataPagamento pgtype.Date `json:"data_pagamento"`
PgtoOk pgtype.Bool `json:"pgto_ok"`
}
func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (FinancialTransaction, error) {
row := q.db.QueryRow(ctx, createTransaction,
arg.FotID,
arg.DataCobranca,
arg.TipoEvento,
arg.TipoServico,
arg.ProfessionalName,
arg.Whatsapp,
arg.Cpf,
arg.TabelaFree,
arg.ValorFree,
arg.ValorExtra,
arg.DescricaoExtra,
arg.TotalPagar,
arg.DataPagamento,
arg.PgtoOk,
)
var i FinancialTransaction
err := row.Scan(
&i.ID,
&i.FotID,
&i.DataCobranca,
&i.TipoEvento,
&i.TipoServico,
&i.ProfessionalName,
&i.Whatsapp,
&i.Cpf,
&i.TabelaFree,
&i.ValorFree,
&i.ValorExtra,
&i.DescricaoExtra,
&i.TotalPagar,
&i.DataPagamento,
&i.PgtoOk,
&i.CriadoEm,
&i.AtualizadoEm,
)
return i, err
}
const deleteTransaction = `-- name: DeleteTransaction :exec
DELETE FROM financial_transactions WHERE id = $1
`
func (q *Queries) DeleteTransaction(ctx context.Context, id pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteTransaction, id)
return err
}
const getTransaction = `-- name: GetTransaction :one
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em FROM financial_transactions WHERE id = $1
`
func (q *Queries) GetTransaction(ctx context.Context, id pgtype.UUID) (FinancialTransaction, error) {
row := q.db.QueryRow(ctx, getTransaction, id)
var i FinancialTransaction
err := row.Scan(
&i.ID,
&i.FotID,
&i.DataCobranca,
&i.TipoEvento,
&i.TipoServico,
&i.ProfessionalName,
&i.Whatsapp,
&i.Cpf,
&i.TabelaFree,
&i.ValorFree,
&i.ValorExtra,
&i.DescricaoExtra,
&i.TotalPagar,
&i.DataPagamento,
&i.PgtoOk,
&i.CriadoEm,
&i.AtualizadoEm,
)
return i, err
}
const listTransactions = `-- name: ListTransactions :many
SELECT t.id, t.fot_id, t.data_cobranca, t.tipo_evento, t.tipo_servico, t.professional_name, t.whatsapp, t.cpf, t.tabela_free, t.valor_free, t.valor_extra, t.descricao_extra, t.total_pagar, t.data_pagamento, t.pgto_ok, t.criado_em, t.atualizado_em, f.fot as fot_numero
FROM financial_transactions t
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
ORDER BY t.data_cobranca DESC
`
type ListTransactionsRow struct {
ID pgtype.UUID `json:"id"`
FotID pgtype.UUID `json:"fot_id"`
DataCobranca pgtype.Date `json:"data_cobranca"`
TipoEvento pgtype.Text `json:"tipo_evento"`
TipoServico pgtype.Text `json:"tipo_servico"`
ProfessionalName pgtype.Text `json:"professional_name"`
Whatsapp pgtype.Text `json:"whatsapp"`
Cpf pgtype.Text `json:"cpf"`
TabelaFree pgtype.Text `json:"tabela_free"`
ValorFree pgtype.Numeric `json:"valor_free"`
ValorExtra pgtype.Numeric `json:"valor_extra"`
DescricaoExtra pgtype.Text `json:"descricao_extra"`
TotalPagar pgtype.Numeric `json:"total_pagar"`
DataPagamento pgtype.Date `json:"data_pagamento"`
PgtoOk pgtype.Bool `json:"pgto_ok"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FotNumero pgtype.Int4 `json:"fot_numero"`
}
func (q *Queries) ListTransactions(ctx context.Context) ([]ListTransactionsRow, error) {
rows, err := q.db.Query(ctx, listTransactions)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListTransactionsRow
for rows.Next() {
var i ListTransactionsRow
if err := rows.Scan(
&i.ID,
&i.FotID,
&i.DataCobranca,
&i.TipoEvento,
&i.TipoServico,
&i.ProfessionalName,
&i.Whatsapp,
&i.Cpf,
&i.TabelaFree,
&i.ValorFree,
&i.ValorExtra,
&i.DescricaoExtra,
&i.TotalPagar,
&i.DataPagamento,
&i.PgtoOk,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FotNumero,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listTransactionsByFot = `-- name: ListTransactionsByFot :many
SELECT id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em FROM financial_transactions
WHERE fot_id = $1
ORDER BY data_cobranca DESC
`
func (q *Queries) ListTransactionsByFot(ctx context.Context, fotID pgtype.UUID) ([]FinancialTransaction, error) {
rows, err := q.db.Query(ctx, listTransactionsByFot, fotID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []FinancialTransaction
for rows.Next() {
var i FinancialTransaction
if err := rows.Scan(
&i.ID,
&i.FotID,
&i.DataCobranca,
&i.TipoEvento,
&i.TipoServico,
&i.ProfessionalName,
&i.Whatsapp,
&i.Cpf,
&i.TabelaFree,
&i.ValorFree,
&i.ValorExtra,
&i.DescricaoExtra,
&i.TotalPagar,
&i.DataPagamento,
&i.PgtoOk,
&i.CriadoEm,
&i.AtualizadoEm,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const sumTotalByFot = `-- name: SumTotalByFot :one
SELECT COALESCE(SUM(total_pagar), 0)::NUMERIC
FROM financial_transactions
WHERE fot_id = $1
`
func (q *Queries) SumTotalByFot(ctx context.Context, fotID pgtype.UUID) (pgtype.Numeric, error) {
row := q.db.QueryRow(ctx, sumTotalByFot, fotID)
var column_1 pgtype.Numeric
err := row.Scan(&column_1)
return column_1, err
}
const updateTransaction = `-- name: UpdateTransaction :one
UPDATE financial_transactions SET
fot_id = $2, data_cobranca = $3, tipo_evento = $4, tipo_servico = $5,
professional_name = $6, whatsapp = $7, cpf = $8, tabela_free = $9,
valor_free = $10, valor_extra = $11, descricao_extra = $12,
total_pagar = $13, data_pagamento = $14, pgto_ok = $15,
atualizado_em = NOW()
WHERE id = $1
RETURNING id, fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name, whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra, total_pagar, data_pagamento, pgto_ok, criado_em, atualizado_em
`
type UpdateTransactionParams struct {
ID pgtype.UUID `json:"id"`
FotID pgtype.UUID `json:"fot_id"`
DataCobranca pgtype.Date `json:"data_cobranca"`
TipoEvento pgtype.Text `json:"tipo_evento"`
TipoServico pgtype.Text `json:"tipo_servico"`
ProfessionalName pgtype.Text `json:"professional_name"`
Whatsapp pgtype.Text `json:"whatsapp"`
Cpf pgtype.Text `json:"cpf"`
TabelaFree pgtype.Text `json:"tabela_free"`
ValorFree pgtype.Numeric `json:"valor_free"`
ValorExtra pgtype.Numeric `json:"valor_extra"`
DescricaoExtra pgtype.Text `json:"descricao_extra"`
TotalPagar pgtype.Numeric `json:"total_pagar"`
DataPagamento pgtype.Date `json:"data_pagamento"`
PgtoOk pgtype.Bool `json:"pgto_ok"`
}
func (q *Queries) UpdateTransaction(ctx context.Context, arg UpdateTransactionParams) (FinancialTransaction, error) {
row := q.db.QueryRow(ctx, updateTransaction,
arg.ID,
arg.FotID,
arg.DataCobranca,
arg.TipoEvento,
arg.TipoServico,
arg.ProfessionalName,
arg.Whatsapp,
arg.Cpf,
arg.TabelaFree,
arg.ValorFree,
arg.ValorExtra,
arg.DescricaoExtra,
arg.TotalPagar,
arg.DataPagamento,
arg.PgtoOk,
)
var i FinancialTransaction
err := row.Scan(
&i.ID,
&i.FotID,
&i.DataCobranca,
&i.TipoEvento,
&i.TipoServico,
&i.ProfessionalName,
&i.Whatsapp,
&i.Cpf,
&i.TabelaFree,
&i.ValorFree,
&i.ValorExtra,
&i.DescricaoExtra,
&i.TotalPagar,
&i.DataPagamento,
&i.PgtoOk,
&i.CriadoEm,
&i.AtualizadoEm,
)
return i, err
}

View file

@ -155,6 +155,26 @@ type Empresa struct {
CriadoEm pgtype.Timestamptz `json:"criado_em"`
}
type FinancialTransaction struct {
ID pgtype.UUID `json:"id"`
FotID pgtype.UUID `json:"fot_id"`
DataCobranca pgtype.Date `json:"data_cobranca"`
TipoEvento pgtype.Text `json:"tipo_evento"`
TipoServico pgtype.Text `json:"tipo_servico"`
ProfessionalName pgtype.Text `json:"professional_name"`
Whatsapp pgtype.Text `json:"whatsapp"`
Cpf pgtype.Text `json:"cpf"`
TabelaFree pgtype.Text `json:"tabela_free"`
ValorFree pgtype.Numeric `json:"valor_free"`
ValorExtra pgtype.Numeric `json:"valor_extra"`
DescricaoExtra pgtype.Text `json:"descricao_extra"`
TotalPagar pgtype.Numeric `json:"total_pagar"`
DataPagamento pgtype.Date `json:"data_pagamento"`
PgtoOk pgtype.Bool `json:"pgto_ok"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
}
type FuncoesProfissionai struct {
ID pgtype.UUID `json:"id"`
Nome string `json:"nome"`
@ -206,6 +226,11 @@ type PrecosTiposEvento struct {
CriadoEm pgtype.Timestamptz `json:"criado_em"`
}
type ProfissionaisFuncoesJunction struct {
ProfissionalID pgtype.UUID `json:"profissional_id"`
FuncaoID pgtype.UUID `json:"funcao_id"`
}
type RefreshToken struct {
ID pgtype.UUID `json:"id"`
UsuarioID pgtype.UUID `json:"usuario_id"`

View file

@ -11,6 +11,31 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const addFunctionToProfessional = `-- name: AddFunctionToProfessional :exec
INSERT INTO profissionais_funcoes_junction (profissional_id, funcao_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING
`
type AddFunctionToProfessionalParams struct {
ProfissionalID pgtype.UUID `json:"profissional_id"`
FuncaoID pgtype.UUID `json:"funcao_id"`
}
func (q *Queries) AddFunctionToProfessional(ctx context.Context, arg AddFunctionToProfessionalParams) error {
_, err := q.db.Exec(ctx, addFunctionToProfessional, arg.ProfissionalID, arg.FuncaoID)
return err
}
const clearProfessionalFunctions = `-- name: ClearProfessionalFunctions :exec
DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1
`
func (q *Queries) ClearProfessionalFunctions(ctx context.Context, profissionalID pgtype.UUID) error {
_, err := q.db.Exec(ctx, clearProfessionalFunctions, profissionalID)
return err
}
const createProfissional = `-- name: CreateProfissional :one
INSERT INTO cadastro_profissionais (
usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp,
@ -117,6 +142,15 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
return i, err
}
const deleteProfessionalFunctions = `-- name: DeleteProfessionalFunctions :exec
DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1
`
func (q *Queries) DeleteProfessionalFunctions(ctx context.Context, profissionalID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteProfessionalFunctions, profissionalID)
return err
}
const deleteProfissional = `-- name: DeleteProfissional :exec
DELETE FROM cadastro_profissionais
WHERE id = $1
@ -128,9 +162,15 @@ func (q *Queries) DeleteProfissional(ctx context.Context, id pgtype.UUID) error
}
const getProfissionalByID = `-- name: GetProfissionalByID :one
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.id = $1 LIMIT 1
`
@ -164,7 +204,7 @@ type GetProfissionalByIDRow struct {
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
Functions interface{} `json:"functions"`
}
func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetProfissionalByIDRow, error) {
@ -200,15 +240,21 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetP
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
&i.Functions,
)
return i, err
}
const getProfissionalByUsuarioID = `-- name: GetProfissionalByUsuarioID :one
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.usuario_id = $1 LIMIT 1
`
@ -242,7 +288,7 @@ type GetProfissionalByUsuarioIDRow struct {
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
Functions interface{} `json:"functions"`
}
func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgtype.UUID) (GetProfissionalByUsuarioIDRow, error) {
@ -278,15 +324,21 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
&i.Functions,
)
return i, err
}
const listProfissionais = `-- name: ListProfissionais :many
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email as usuario_email
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, u.email as usuario_email,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
LEFT JOIN usuarios u ON p.usuario_id = u.id
ORDER BY p.nome
`
@ -321,8 +373,8 @@ type ListProfissionaisRow struct {
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
FuncaoNome pgtype.Text `json:"funcao_nome"`
UsuarioEmail pgtype.Text `json:"usuario_email"`
Functions interface{} `json:"functions"`
}
func (q *Queries) ListProfissionais(ctx context.Context) ([]ListProfissionaisRow, error) {
@ -364,8 +416,221 @@ func (q *Queries) ListProfissionais(ctx context.Context) ([]ListProfissionaisRow
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.FuncaoNome,
&i.UsuarioEmail,
&i.Functions,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchProfissionais = `-- name: SearchProfissionais :many
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
WHERE p.nome ILIKE '%' || $1 || '%'
ORDER BY p.nome
LIMIT 20
`
type SearchProfissionaisRow struct {
ID pgtype.UUID `json:"id"`
UsuarioID pgtype.UUID `json:"usuario_id"`
Nome string `json:"nome"`
FuncaoProfissionalID pgtype.UUID `json:"funcao_profissional_id"`
Endereco pgtype.Text `json:"endereco"`
Cidade pgtype.Text `json:"cidade"`
Uf pgtype.Text `json:"uf"`
Whatsapp pgtype.Text `json:"whatsapp"`
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"`
ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"`
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
TipoCartao pgtype.Text `json:"tipo_cartao"`
Observacao pgtype.Text `json:"observacao"`
QualTec pgtype.Int4 `json:"qual_tec"`
EducacaoSimpatia pgtype.Int4 `json:"educacao_simpatia"`
DesempenhoEvento pgtype.Int4 `json:"desempenho_evento"`
DispHorario pgtype.Int4 `json:"disp_horario"`
Media pgtype.Numeric `json:"media"`
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
Email pgtype.Text `json:"email"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Functions interface{} `json:"functions"`
}
func (q *Queries) SearchProfissionais(ctx context.Context, dollar_1 pgtype.Text) ([]SearchProfissionaisRow, error) {
rows, err := q.db.Query(ctx, searchProfissionais, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchProfissionaisRow
for rows.Next() {
var i SearchProfissionaisRow
if err := rows.Scan(
&i.ID,
&i.UsuarioID,
&i.Nome,
&i.FuncaoProfissionalID,
&i.Endereco,
&i.Cidade,
&i.Uf,
&i.Whatsapp,
&i.CpfCnpjTitular,
&i.Banco,
&i.Agencia,
&i.ContaPix,
&i.CarroDisponivel,
&i.TemEstudio,
&i.QtdEstudio,
&i.TipoCartao,
&i.Observacao,
&i.QualTec,
&i.EducacaoSimpatia,
&i.DesempenhoEvento,
&i.DispHorario,
&i.Media,
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.Email,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.Functions,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const searchProfissionaisByFunction = `-- name: SearchProfissionaisByFunction :many
SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em,
COALESCE(
(SELECT json_agg(json_build_object('id', f2.id, 'nome', f2.nome))
FROM profissionais_funcoes_junction pfj2
JOIN funcoes_profissionais f2 ON pfj2.funcao_id = f2.id
WHERE pfj2.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
WHERE (p.nome ILIKE '%' || $1 || '%')
AND (
EXISTS (
SELECT 1
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id AND f.nome = $2
)
OR
p.funcao_profissional_id = (SELECT id FROM funcoes_profissionais WHERE nome = $2 LIMIT 1)
)
ORDER BY p.nome
LIMIT 20
`
type SearchProfissionaisByFunctionParams struct {
Column1 pgtype.Text `json:"column_1"`
Nome string `json:"nome"`
}
type SearchProfissionaisByFunctionRow struct {
ID pgtype.UUID `json:"id"`
UsuarioID pgtype.UUID `json:"usuario_id"`
Nome string `json:"nome"`
FuncaoProfissionalID pgtype.UUID `json:"funcao_profissional_id"`
Endereco pgtype.Text `json:"endereco"`
Cidade pgtype.Text `json:"cidade"`
Uf pgtype.Text `json:"uf"`
Whatsapp pgtype.Text `json:"whatsapp"`
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"`
ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"`
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
TipoCartao pgtype.Text `json:"tipo_cartao"`
Observacao pgtype.Text `json:"observacao"`
QualTec pgtype.Int4 `json:"qual_tec"`
EducacaoSimpatia pgtype.Int4 `json:"educacao_simpatia"`
DesempenhoEvento pgtype.Int4 `json:"desempenho_evento"`
DispHorario pgtype.Int4 `json:"disp_horario"`
Media pgtype.Numeric `json:"media"`
TabelaFree pgtype.Text `json:"tabela_free"`
ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"`
Equipamentos pgtype.Text `json:"equipamentos"`
Email pgtype.Text `json:"email"`
AvatarUrl pgtype.Text `json:"avatar_url"`
CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Functions interface{} `json:"functions"`
}
func (q *Queries) SearchProfissionaisByFunction(ctx context.Context, arg SearchProfissionaisByFunctionParams) ([]SearchProfissionaisByFunctionRow, error) {
rows, err := q.db.Query(ctx, searchProfissionaisByFunction, arg.Column1, arg.Nome)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchProfissionaisByFunctionRow
for rows.Next() {
var i SearchProfissionaisByFunctionRow
if err := rows.Scan(
&i.ID,
&i.UsuarioID,
&i.Nome,
&i.FuncaoProfissionalID,
&i.Endereco,
&i.Cidade,
&i.Uf,
&i.Whatsapp,
&i.CpfCnpjTitular,
&i.Banco,
&i.Agencia,
&i.ContaPix,
&i.CarroDisponivel,
&i.TemEstudio,
&i.QtdEstudio,
&i.TipoCartao,
&i.Observacao,
&i.QualTec,
&i.EducacaoSimpatia,
&i.DesempenhoEvento,
&i.DispHorario,
&i.Media,
&i.TabelaFree,
&i.ExtraPorEquipamento,
&i.Equipamentos,
&i.Email,
&i.AvatarUrl,
&i.CriadoEm,
&i.AtualizadoEm,
&i.Functions,
); err != nil {
return nil, err
}

View file

@ -89,6 +89,27 @@ func (q *Queries) GetPreco(ctx context.Context, arg GetPrecoParams) (PrecosTipos
return i, err
}
const getStandardPrice = `-- name: GetStandardPrice :one
SELECT p.valor
FROM precos_tipos_eventos p
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE te.nome = $1 AND f.nome = $2
LIMIT 1
`
type GetStandardPriceParams struct {
Nome string `json:"nome"`
Nome_2 string `json:"nome_2"`
}
func (q *Queries) GetStandardPrice(ctx context.Context, arg GetStandardPriceParams) (pgtype.Numeric, error) {
row := q.db.QueryRow(ctx, getStandardPrice, arg.Nome, arg.Nome_2)
var valor pgtype.Numeric
err := row.Scan(&valor)
return valor, err
}
const getTipoEventoByID = `-- name: GetTipoEventoByID :one
SELECT id, nome, criado_em FROM tipos_eventos WHERE id = $1
`

View file

@ -0,0 +1,18 @@
-- Migration to support multiple functions per professional
-- 1. Create Junction Table
CREATE TABLE IF NOT EXISTS profissionais_funcoes_junction (
profissional_id UUID NOT NULL REFERENCES cadastro_profissionais(id) ON DELETE CASCADE,
funcao_id UUID NOT NULL REFERENCES funcoes_profissionais(id) ON DELETE CASCADE,
PRIMARY KEY (profissional_id, funcao_id)
);
-- 2. Migrate existing data (assuming column exists)
INSERT INTO profissionais_funcoes_junction (profissional_id, funcao_id)
SELECT id, funcao_profissional_id
FROM cadastro_profissionais
WHERE funcao_profissional_id IS NOT NULL
ON CONFLICT DO NOTHING;
-- 3. (Optional) Drop the old column later. keeping it for backward compat for a moment, or handle it in Go.
-- ALTER TABLE cadastro_profissionais DROP COLUMN funcao_profissional_id;

View file

@ -188,4 +188,13 @@ JOIN agenda a ON ap.agenda_id = a.id
WHERE ap.profissional_id = $1
AND a.data_evento = $2
AND ap.status = 'ACEITO'
AND a.id != $3;
AND a.id != $3;
-- name: ListAgendasByFot :many
SELECT
a.*,
te.nome as tipo_evento_nome
FROM agenda a
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
WHERE a.fot_id = $1
ORDER BY a.data_evento;

View file

@ -63,3 +63,37 @@ RETURNING *;
-- name: DeleteCadastroFot :exec
DELETE FROM cadastro_fot WHERE id = $1;
-- name: GetCadastroFotByFotJoin :one
SELECT
c.*,
e.nome as empresa_nome,
cur.nome as curso_nome,
a.ano_semestre as ano_formatura_label
FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.fot = $1;
-- name: UpdateCadastroFotGastos :exec
UPDATE cadastro_fot SET
gastos_captacao = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: SearchFot :many
SELECT
c.*,
e.nome as empresa_nome,
cur.nome as curso_nome,
a.ano_semestre as ano_formatura_label
FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%'
ORDER BY c.fot ASC
LIMIT 10;

View file

@ -0,0 +1,41 @@
-- name: CreateTransaction :one
INSERT INTO financial_transactions (
fot_id, data_cobranca, tipo_evento, tipo_servico, professional_name,
whatsapp, cpf, tabela_free, valor_free, valor_extra, descricao_extra,
total_pagar, data_pagamento, pgto_ok
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
) RETURNING *;
-- name: ListTransactionsByFot :many
SELECT * FROM financial_transactions
WHERE fot_id = $1
ORDER BY data_cobranca DESC;
-- name: ListTransactions :many
SELECT t.*, f.fot as fot_numero
FROM financial_transactions t
LEFT JOIN cadastro_fot f ON t.fot_id = f.id
ORDER BY t.data_cobranca DESC;
-- name: SumTotalByFot :one
SELECT COALESCE(SUM(total_pagar), 0)::NUMERIC
FROM financial_transactions
WHERE fot_id = $1;
-- name: UpdateTransaction :one
UPDATE financial_transactions SET
fot_id = $2, data_cobranca = $3, tipo_evento = $4, tipo_servico = $5,
professional_name = $6, whatsapp = $7, cpf = $8, tabela_free = $9,
valor_free = $10, valor_extra = $11, descricao_extra = $12,
total_pagar = $13, data_pagamento = $14, pgto_ok = $15,
atualizado_em = NOW()
WHERE id = $1
RETURNING *;
-- name: DeleteTransaction :exec
DELETE FROM financial_transactions WHERE id = $1;
-- name: GetTransaction :one
SELECT * FROM financial_transactions WHERE id = $1;

View file

@ -11,21 +11,39 @@ INSERT INTO cadastro_profissionais (
) RETURNING *;
-- name: GetProfissionalByUsuarioID :one
SELECT p.*, f.nome as funcao_nome
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.usuario_id = $1 LIMIT 1;
-- name: GetProfissionalByID :one
SELECT p.*, f.nome as funcao_nome
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE p.id = $1 LIMIT 1;
-- name: ListProfissionais :many
SELECT p.*, f.nome as funcao_nome, u.email as usuario_email
SELECT p.*, u.email as usuario_email,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
LEFT JOIN usuarios u ON p.usuario_id = u.id
ORDER BY p.nome;
@ -64,3 +82,52 @@ RETURNING *;
-- name: DeleteProfissional :exec
DELETE FROM cadastro_profissionais
WHERE id = $1;
-- name: SearchProfissionais :many
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
WHERE p.nome ILIKE '%' || $1 || '%'
ORDER BY p.nome
LIMIT 20;
-- name: SearchProfissionaisByFunction :many
SELECT p.*,
COALESCE(
(SELECT json_agg(json_build_object('id', f2.id, 'nome', f2.nome))
FROM profissionais_funcoes_junction pfj2
JOIN funcoes_profissionais f2 ON pfj2.funcao_id = f2.id
WHERE pfj2.profissional_id = p.id
), '[]'::json
) as functions
FROM cadastro_profissionais p
WHERE (p.nome ILIKE '%' || $1 || '%')
AND (
EXISTS (
SELECT 1
FROM profissionais_funcoes_junction pfj
JOIN funcoes_profissionais f ON pfj.funcao_id = f.id
WHERE pfj.profissional_id = p.id AND f.nome = $2
)
OR
p.funcao_profissional_id = (SELECT id FROM funcoes_profissionais WHERE nome = $2 LIMIT 1)
)
ORDER BY p.nome
LIMIT 20;
-- name: AddFunctionToProfessional :exec
INSERT INTO profissionais_funcoes_junction (profissional_id, funcao_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING;
-- name: ClearProfessionalFunctions :exec
DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1;
-- name: DeleteProfessionalFunctions :exec
DELETE FROM profissionais_funcoes_junction WHERE profissional_id = $1;

View file

@ -31,3 +31,11 @@ SELECT * FROM precos_tipos_eventos WHERE tipo_evento_id = $1 AND funcao_profissi
-- name: DeletePreco :exec
DELETE FROM precos_tipos_eventos WHERE id = $1;
-- name: GetStandardPrice :one
SELECT p.valor
FROM precos_tipos_eventos p
JOIN tipos_eventos te ON p.tipo_evento_id = te.id
JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
WHERE te.nome = $1 AND f.nome = $2
LIMIT 1;

View file

@ -58,6 +58,12 @@ CREATE TABLE IF NOT EXISTS cadastro_profissionais (
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS profissionais_funcoes_junction (
profissional_id UUID NOT NULL REFERENCES cadastro_profissionais(id) ON DELETE CASCADE,
funcao_id UUID NOT NULL REFERENCES funcoes_profissionais(id) ON DELETE CASCADE,
PRIMARY KEY (profissional_id, funcao_id)
);
CREATE TABLE IF NOT EXISTS refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
usuario_id UUID NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
@ -433,3 +439,25 @@ CREATE TABLE IF NOT EXISTS codigos_acesso (
ativo BOOLEAN NOT NULL DEFAULT TRUE,
usos INT NOT NULL DEFAULT 0
);
-- Financeiro Extrato
CREATE TABLE IF NOT EXISTS financial_transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
fot_id UUID REFERENCES cadastro_fot(id) ON DELETE SET NULL,
data_cobranca DATE,
tipo_evento VARCHAR(100),
tipo_servico VARCHAR(100),
professional_name VARCHAR(255),
whatsapp VARCHAR(50),
cpf VARCHAR(20),
tabela_free VARCHAR(50),
valor_free NUMERIC(10,2) DEFAULT 0,
valor_extra NUMERIC(10,2) DEFAULT 0,
descricao_extra TEXT,
total_pagar NUMERIC(10,2) DEFAULT 0,
data_pagamento DATE,
pgto_ok BOOLEAN DEFAULT FALSE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View file

@ -0,0 +1,295 @@
package finance
import (
"fmt"
"net/http"
"photum-backend/internal/db/generated"
"strconv"
"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")
fotNum, err := strconv.Atoi(fotNumStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid FOT Number"})
return
}
fotData, err := h.service.AutoFillSearch(c.Request.Context(), int32(fotNum))
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)
}

View file

@ -0,0 +1,111 @@
package finance
import (
"context"
"photum-backend/internal/db/generated"
"github.com/jackc/pgx/v5/pgtype"
)
type Service struct {
queries *generated.Queries
}
func NewService(queries *generated.Queries) *Service {
return &Service{queries: queries}
}
func (s *Service) Create(ctx context.Context, params generated.CreateTransactionParams) (generated.FinancialTransaction, error) {
txn, err := s.queries.CreateTransaction(ctx, params)
if err != nil {
return generated.FinancialTransaction{}, err
}
if params.FotID.Valid {
_ = s.updateFotExpenses(ctx, params.FotID)
}
return txn, nil
}
func (s *Service) Update(ctx context.Context, params generated.UpdateTransactionParams) (generated.FinancialTransaction, error) {
txn, err := s.queries.UpdateTransaction(ctx, params)
if err != nil {
return generated.FinancialTransaction{}, err
}
// Recalculate for the new FOT (if changed, we should technically recalc old one too, but simpler for now)
if params.FotID.Valid {
_ = s.updateFotExpenses(ctx, params.FotID)
}
return txn, nil
}
func (s *Service) Delete(ctx context.Context, id pgtype.UUID) error {
// First fetch to get FotID
txn, err := s.queries.GetTransaction(ctx, id)
if err != nil {
return err
}
err = s.queries.DeleteTransaction(ctx, id)
if err != nil {
return err
}
if txn.FotID.Valid {
_ = s.updateFotExpenses(ctx, txn.FotID)
}
return nil
}
func (s *Service) ListByFot(ctx context.Context, fotID pgtype.UUID) ([]generated.FinancialTransaction, error) {
return s.queries.ListTransactionsByFot(ctx, fotID)
}
func (s *Service) ListAll(ctx context.Context) ([]generated.ListTransactionsRow, error) {
return s.queries.ListTransactions(ctx)
}
func (s *Service) AutoFillSearch(ctx context.Context, fotNumber int32) (generated.GetCadastroFotByFotJoinRow, error) {
return s.queries.GetCadastroFotByFotJoin(ctx, fotNumber)
}
func (s *Service) ListFotEvents(ctx context.Context, fotID pgtype.UUID) ([]generated.ListAgendasByFotRow, error) {
return s.queries.ListAgendasByFot(ctx, fotID)
}
func (s *Service) SearchProfessionals(ctx context.Context, query string) ([]generated.SearchProfissionaisRow, error) {
return s.queries.SearchProfissionais(ctx, pgtype.Text{String: query, Valid: true})
}
func (s *Service) SearchProfessionalsByFunction(ctx context.Context, query string, functionName string) ([]generated.SearchProfissionaisByFunctionRow, error) {
return s.queries.SearchProfissionaisByFunction(ctx, generated.SearchProfissionaisByFunctionParams{
Column1: pgtype.Text{String: query, Valid: true}, // $1 - Name
Nome: functionName, // $2 - Function Name
})
}
func (s *Service) GetStandardPrice(ctx context.Context, eventName string, serviceName string) (pgtype.Numeric, error) {
// serviceName here is the Function Name (e.g. Fotógrafo)
return s.queries.GetStandardPrice(ctx, generated.GetStandardPriceParams{
Nome: eventName, // $1 - Event Name
Nome_2: serviceName, // $2 - Function Name
})
}
func (s *Service) SearchFot(ctx context.Context, query string) ([]generated.SearchFotRow, error) {
return s.queries.SearchFot(ctx, pgtype.Text{String: query, Valid: true})
}
func (s *Service) updateFotExpenses(ctx context.Context, fotID pgtype.UUID) error {
total, err := s.queries.SumTotalByFot(ctx, fotID)
if err != nil {
return err
}
return s.queries.UpdateCadastroFotGastos(ctx, generated.UpdateCadastroFotGastosParams{
ID: fotID,
GastosCaptacao: total,
})
}

View file

@ -1,6 +1,7 @@
package profissionais
import (
"encoding/json"
"net/http"
"photum-backend/internal/db/generated"
@ -20,34 +21,35 @@ func NewHandler(service *Service) *Handler {
// 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"` // Now returns name from join
FuncaoProfissionalID string `json:"funcao_profissional_id"`
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"`
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 {
@ -64,6 +66,7 @@ func toResponse(p interface{}) ProfissionalResponse {
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),
@ -98,7 +101,8 @@ func toResponse(p interface{}) ProfissionalResponse {
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
Nome: v.Nome,
FuncaoProfissionalID: uuid.UUID(v.FuncaoProfissionalID.Bytes).String(),
FuncaoProfissional: v.FuncaoNome.String, // From join
FuncaoProfissional: "", // v.FuncaoNome removed from query or changed?
Functions: toJSONRaw(v.Functions),
Endereco: fromPgText(v.Endereco),
Cidade: fromPgText(v.Cidade),
Uf: fromPgText(v.Uf),
@ -129,7 +133,8 @@ func toResponse(p interface{}) ProfissionalResponse {
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
Nome: v.Nome,
FuncaoProfissionalID: uuid.UUID(v.FuncaoProfissionalID.Bytes).String(),
FuncaoProfissional: v.FuncaoNome.String, // From join
FuncaoProfissional: "", // v.FuncaoNome removed
Functions: toJSONRaw(v.Functions),
Endereco: fromPgText(v.Endereco),
Cidade: fromPgText(v.Cidade),
Uf: fromPgText(v.Uf),
@ -191,6 +196,28 @@ func fromPgNumeric(n pgtype.Numeric) *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

View file

@ -22,6 +22,7 @@ func NewService(queries *generated.Queries) *Service {
type CreateProfissionalInput struct {
Nome string `json:"nome"`
FuncaoProfissionalID string `json:"funcao_profissional_id"`
FuncoesIds []string `json:"funcoes_ids"` // New field
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
@ -105,6 +106,26 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
if err != nil {
return nil, err
}
// Insert multiple functions if provided
if len(input.FuncoesIds) > 0 {
for _, fid := range input.FuncoesIds {
fUUID, err := uuid.Parse(fid)
if err == nil {
_ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{
ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true},
FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true},
})
}
}
} else if funcaoValid {
// If no list provided but single ID is, insert that one too into junction
_ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{
ProfissionalID: pgtype.UUID{Bytes: prof.ID.Bytes, Valid: true},
FuncaoID: pgtype.UUID{Bytes: funcaoUUID, Valid: true},
})
}
return &prof, nil
}
@ -127,6 +148,7 @@ func (s *Service) GetByID(ctx context.Context, id string) (*generated.GetProfiss
type UpdateProfissionalInput struct {
Nome string `json:"nome"`
FuncaoProfissionalID string `json:"funcao_profissional_id"`
FuncoesIds []string `json:"funcoes_ids"` // New field
Endereco *string `json:"endereco"`
Cidade *string `json:"cidade"`
Uf *string `json:"uf"`
@ -196,6 +218,31 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
if err != nil {
return nil, err
}
// Update functions logic
// If input.FuncoesIds is provided (even empty), replace all.
// If nil, maybe keep existing? For simplicity, let's assume if present we update.
// Actually frontend should send full list.
if input.FuncoesIds != nil {
// Clear existing
_ = s.queries.ClearProfessionalFunctions(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
// Add new
for _, fid := range input.FuncoesIds {
fUUID, err := uuid.Parse(fid)
if err == nil {
_ = s.queries.AddFunctionToProfessional(ctx, generated.AddFunctionToProfessionalParams{
ProfissionalID: pgtype.UUID{Bytes: uuidVal, Valid: true},
FuncaoID: pgtype.UUID{Bytes: fUUID, Valid: true},
})
}
}
} else if input.FuncaoProfissionalID != "" {
// If legacy field matches, ensure it's in junction set too?
// Or maybe we treat legacy ID as primary and sync it.
// For now, let's just make sure at least one exists if provided.
// But usually update sends all data.
}
return &prof, nil
}

View file

@ -45,24 +45,32 @@ export const EventTable: React.FC<EventTableProps> = ({
const calculateTeamStatus = (event: EventData) => {
const assignments = event.assignments || [];
// Helper to check if professional has a specific role
const hasRole = (professional: any, roleSlug: string) => {
if (!professional) return false;
const term = roleSlug.toLowerCase();
// Check functions array first (new multi-role system)
if (professional.functions && professional.functions.length > 0) {
return professional.functions.some((f: any) => f.nome.toLowerCase().includes(term));
}
// Fallback to legacy role field
return (professional.role || "").toLowerCase().includes(term);
};
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("fot");
}).length;
const acceptedFotografos = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "fot")
).length;
const acceptedRecepcionistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("recep");
}).length;
const acceptedRecepcionistas = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "recep")
).length;
const acceptedCinegrafistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("cine");
}).length;
const acceptedCinegrafistas = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "cine")
).length;
// Quantidades necessárias
const qtdFotografos = event.qtdFotografos || 0;

View file

@ -89,10 +89,21 @@ export const ProfessionalDetailsModal: React.FC<ProfessionalDetailsModalProps> =
<div className="text-center sm:text-left w-full">
<h2 className="text-2xl font-serif font-bold text-brand-black">{professional.name || professional.nome}</h2>
<div className="flex flex-wrap justify-center sm:justify-start gap-2 mt-2">
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20 flex items-center gap-2">
<User size={14} />
{professional.role || "Profissional"}
</span>
<div className="flex flex-wrap items-center justify-center sm:justify-start gap-2">
{professional.functions && professional.functions.length > 0 ? (
professional.functions.map(f => (
<span key={f.id} className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20 flex items-center gap-2">
<User size={14} />
{f.nome}
</span>
))
) : (
<span className="px-3 py-1 bg-brand-gold/10 text-brand-black rounded-full text-sm font-medium border border-brand-gold/20 flex items-center gap-2">
<User size={14} />
{professional.role || "Profissional"}
</span>
)}
</div>
{/* Performance Rating - Only for Master (Admin/Owner), NOT for the professional themselves */}
{isMaster && professional.media !== undefined && professional.media !== null && (
<span className="px-3 py-1 bg-yellow-50 text-yellow-700 rounded-full text-sm font-medium border border-yellow-200 flex items-center gap-1">

View file

@ -15,6 +15,7 @@ export interface ProfessionalData {
confirmarSenha: string;
avatar?: File | null;
funcaoId: string;
funcoesIds: string[]; // New
cep: string;
rua: string;
numero: string;
@ -55,6 +56,7 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
confirmarSenha: "",
avatar: null,
funcaoId: "",
funcoesIds: [],
cep: "",
rua: "",
numero: "",
@ -331,19 +333,35 @@ export const ProfessionalForm: React.FC<ProfessionalFormProps> = ({
rodando.
</p>
) : (
<select
required
value={formData.funcaoId}
onChange={(e) => handleChange("funcaoId", e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent"
>
<option value="">Selecione uma função</option>
{functions.map((func) => (
<option key={func.id} value={func.id}>
{func.nome}
</option>
))}
</select>
<div className="flex flex-wrap gap-4 bg-gray-50 p-4 rounded-lg border border-gray-200">
{functions.map((func) => (
<label key={func.id} className="flex items-center gap-2 cursor-pointer bg-white px-3 py-2 rounded shadow-sm border border-gray-100 hover:border-brand-gold transition-colors">
<input
type="checkbox"
value={func.id}
checked={formData.funcoesIds?.includes(func.id)}
onChange={(e) => {
const checked = e.target.checked;
const currentIds = formData.funcoesIds || [];
let newIds: string[] = [];
if (checked) {
newIds = [...currentIds, func.id];
} else {
newIds = currentIds.filter((id) => id !== func.id);
}
setFormData((prev) => ({
...prev,
funcoesIds: newIds,
funcaoId: newIds.length > 0 ? newIds[0] : "", // Update primary
funcaoLabel: functions.find(f=>f.id === (newIds.length > 0 ? newIds[0] : ""))?.nome
}));
}}
className="w-5 h-5 text-[#B9CF33] rounded border-gray-300 focus:ring-[#B9CF33]"
/>
<span className="text-sm text-gray-700 font-medium">{func.nome}</span>
</label>
))}
</div>
)}
</div>
</div>

View file

@ -609,6 +609,7 @@ interface DataContextType {
professionals: Professional[];
respondToAssignment: (eventId: string, status: string, reason?: string) => Promise<void>;
updateEventDetails: (id: string, data: any) => Promise<void>;
functions: { id: string; nome: string }[];
}
const DataContext = createContext<DataContextType | undefined>(undefined);
@ -625,6 +626,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
const [professionals, setProfessionals] = useState<Professional[]>([]);
const [functions, setFunctions] = useState<{ id: string; nome: string }[]>([]);
// Fetch events from API
useEffect(() => {
@ -635,7 +637,13 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
if (visibleToken) {
try {
// Import dynamic to avoid circular dependency if any, or just use imported service
const { getAgendas } = await import("../services/apiService");
const { getAgendas, getFunctions } = await import("../services/apiService");
// Fetch Functions (Roles)
getFunctions().then(res => {
if (res.data) setFunctions(res.data);
});
const result = await getAgendas(visibleToken);
console.log("Raw Agenda Data:", result.data); // Debug logging
if (result.data) {
@ -688,6 +696,17 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
attendees: e.qtd_formandos,
fotId: e.fot_id, // UUID
// Resource Mapping
qtdFormandos: e.qtd_formandos,
qtdFotografos: e.qtd_fotografos,
qtdRecepcionistas: e.qtd_recepcionistas,
qtdCinegrafistas: e.qtd_cinegrafistas,
qtdEstudios: e.qtd_estudios,
qtdPontosFoto: e.qtd_ponto_foto,
qtdPontosDecorados: e.qtd_ponto_decorado,
qtdPontosLed: e.qtd_pontos_led,
qtdPlataforma360: e.qtd_plataforma_360,
// Joined Fields
fot: e.fot_numero ?? e.fot_id, // Show Number if available (even 0), else ID
curso: e.curso_nome,
@ -803,6 +822,9 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
tabela_free: p.tabela_free,
extra_por_equipamento: p.extra_por_equipamento,
equipamentos: p.equipamentos,
// Multi-function support
functions: p.functions || [],
availability: {}, // Default empty availability
}));
@ -1140,6 +1162,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
},
addEvent,
updateEventStatus,
functions,
assignPhotographer,
getEventsByRole,
addInstitution,

View file

@ -46,6 +46,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
getActiveCoursesByInstitutionId,
respondToAssignment,
updateEventDetails,
functions,
} = useData();
// ... (inside component)
@ -123,23 +124,44 @@ export const Dashboard: React.FC<DashboardProps> = ({
const assignments = event.assignments || [];
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("fot");
}).length;
// Helper to check if professional has a specific role
const hasRole = (professional: Professional | undefined, roleSlug: string) => {
if (!professional) return false;
const term = roleSlug.toLowerCase();
// Check functions array first (new multi-role system)
if (professional.functions && professional.functions.length > 0) {
return professional.functions.some(f => f.nome.toLowerCase().includes(term));
}
// Fallback to legacy role field
return (professional.role || "").toLowerCase().includes(term);
};
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "fot")
).length;
const acceptedRecepcionistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("recep");
}).length;
const pendingFotografos = assignments.filter(a =>
a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "fot")
).length;
const acceptedCinegrafistas = assignments.filter(a => {
if (a.status !== "ACEITO") return false;
const professional = professionals.find(p => p.id === a.professionalId);
return professional && (professional.role || "").toLowerCase().includes("cine");
}).length;
const acceptedRecepcionistas = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "recep")
).length;
const pendingRecepcionistas = assignments.filter(a =>
a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "recep")
).length;
const acceptedCinegrafistas = assignments.filter(a =>
a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "cine")
).length;
const pendingCinegrafistas = assignments.filter(a =>
a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "cine")
).length;
// Quantidades necessárias
const qtdFotografos = event.qtdFotografos || 0;
@ -156,8 +178,11 @@ export const Dashboard: React.FC<DashboardProps> = ({
return {
acceptedFotografos,
pendingFotografos,
acceptedRecepcionistas,
pendingRecepcionistas,
acceptedCinegrafistas,
pendingCinegrafistas,
fotoFaltante,
recepFaltante,
cineFaltante,
@ -179,6 +204,12 @@ export const Dashboard: React.FC<DashboardProps> = ({
if (!selectedEvent) return professionals;
return professionals.filter((professional) => {
// Filter out professionals with unknown roles (matches Team.tsx logic)
if (functions.length > 0) {
const isValidRole = functions.some(f => f.id === professional.funcao_profissional_id);
if (!isValidRole) return false;
}
// Filtro por busca (nome ou email)
if (teamSearchTerm) {
const searchLower = teamSearchTerm.toLowerCase();
@ -316,6 +347,69 @@ export const Dashboard: React.FC<DashboardProps> = ({
reason?: string
) => {
e.stopPropagation();
// Validação de Lotação da Equipe (Apenas para Aceite)
if (status === "ACEITO") {
const targetEvent = events.find(evt => evt.id === eventId);
const currentProfessional = professionals.find(p => p.usuarioId === user.id);
if (targetEvent && currentProfessional) {
// Reutilizando lógica de contagem (adaptada de calculateTeamStatus)
// Precisamos recalcular pois calculateTeamStatus depende de selectedEvent que pode não ser o alvo aqui
const assignments = targetEvent.assignments || [];
const hasRole = (professional: Professional | undefined, roleSlug: string) => {
if (!professional) return false;
const term = roleSlug.toLowerCase();
if (professional.functions && professional.functions.length > 0) {
return professional.functions.some(f => f.nome.toLowerCase().includes(term));
}
return (professional.role || "").toLowerCase().includes(term);
};
const checkQuota = (roleSlugs: string[], acceptedCount: number, requiredCount: number, roleName: string) => {
// Verifica se o profissional tem essa função
const isProfessionalRole = roleSlugs.some(slug => hasRole(currentProfessional, slug));
if (isProfessionalRole) {
// Se já está cheio (ou excedido), bloqueia
if (acceptedCount >= requiredCount) {
return `A equipe de ${roleName} já está completa (${acceptedCount}/${requiredCount}).`;
}
}
return null;
};
// Contagens Atuais
const acceptedFot = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "fot")).length;
const acceptedRecep = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "recep")).length;
const acceptedCine = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "cine")).length;
// Limites
const reqFot = targetEvent.qtdFotografos || 0;
const reqRecep = targetEvent.qtdRecepcionistas || 0;
const reqCine = targetEvent.qtdCinegrafistas || 0;
// Verificações
const errors = [];
const errFot = checkQuota(["fot"], acceptedFot, reqFot, "Fotografia");
if (errFot) errors.push(errFot);
const errRecep = checkQuota(["recep"], acceptedRecep, reqRecep, "Recepção");
if (errRecep) errors.push(errRecep);
const errCine = checkQuota(["cine"], acceptedCine, reqCine, "Cinegrafia");
if (errCine) errors.push(errCine);
if (errors.length > 0) {
alert(`Não foi possível aceitar o convite:\n\n${errors.join("\n")}\n\nEntre em contato com a administração.`);
return; // Bloqueia a ação
}
}
}
await respondToAssignment(eventId, status, reason);
};
@ -756,65 +850,124 @@ export const Dashboard: React.FC<DashboardProps> = ({
QTD Formandos
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdFormandos || selectedEvent.attendees || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Fotógrafo
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdFotografos || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Recepcionista
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdRecepcionistas || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Cinegrafista
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdCinegrafistas || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Estúdio
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdEstudios || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto de Foto
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosFoto || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto Decorado
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosDecorados || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto LED
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosLed || "-"}
{selectedEvent.qtdFormandos || (selectedEvent as any).qtd_formandos || selectedEvent.attendees || "-"}
</td>
</tr>
{/* Helper to calculate pending counts */
(() => {
const teamStatus = calculateTeamStatus(selectedEvent);
const renderResourceRow = (label: string, confirmed: number, pending: number, required: number) => {
if (!required && confirmed === 0 && pending === 0) {
return (
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
{label}
</td>
<td className="px-4 py-3 text-sm text-gray-500">-</td>
</tr>
);
}
const isComplete = confirmed >= required;
return (
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
{label}
</td>
<td className="px-4 py-3 text-sm text-gray-900">
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className={`font-bold ${isComplete ? 'text-green-600' : 'text-gray-900'}`}>{confirmed}</span>
<span className="text-gray-400">/</span>
<span className="font-medium">{required || 0}</span>
{isComplete && <CheckCircle size={14} className="text-green-500 ml-1" />}
</div>
{pending > 0 && (
<span className="text-[10px] text-yellow-600 font-medium">
({pending} aguardando aceite)
</span>
)}
</div>
</td>
</tr>
);
};
return (
<>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
QTD Formandos
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdFormandos || (selectedEvent as any).qtd_formandos || selectedEvent.attendees || "-"}
</td>
</tr>
{renderResourceRow(
"Fotógrafo",
teamStatus.acceptedFotografos,
teamStatus.pendingFotografos,
selectedEvent.qtdFotografos || 0
)}
{renderResourceRow(
"Recepcionista",
teamStatus.acceptedRecepcionistas,
teamStatus.pendingRecepcionistas,
selectedEvent.qtdRecepcionistas || 0
)}
{renderResourceRow(
"Cinegrafista",
teamStatus.acceptedCinegrafistas,
teamStatus.pendingCinegrafistas,
selectedEvent.qtdCinegrafistas || 0
)}
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Estúdio
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdEstudios || (selectedEvent as any).qtd_estudios || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto de Foto
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosFoto || (selectedEvent as any).qtd_ponto_foto || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto Decorado
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosDecorados || (selectedEvent as any).qtd_ponto_decorado || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ponto LED
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPontosLed || (selectedEvent as any).qtd_pontos_led || "-"}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Plataforma 360
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.qtdPlataforma360 || (selectedEvent as any).qtd_plataforma_360 || "-"}
</td>
</tr>
</>
);
})()}
{/* Status e Faltantes */}
{(() => {
@ -1305,7 +1458,9 @@ export const Dashboard: React.FC<DashboardProps> = ({
{/* Função */}
<td className="p-4 text-center">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{photographer.role}
{photographer.functions && photographer.functions.length > 0
? photographer.functions.map(f => f.nome).join(", ")
: photographer.role}
</span>
</td>
@ -1421,7 +1576,9 @@ export const Dashboard: React.FC<DashboardProps> = ({
<div className="flex flex-wrap gap-2 mb-4">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{photographer.role}
{photographer.functions && photographer.functions.length > 0
? photographer.functions.map(f => f.nome).join(", ")
: photographer.role}
</span>
{status === "ACEITO" && (

File diff suppressed because it is too large Load diff

View file

@ -73,6 +73,7 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
equipamentos: professionalData.equipamentos,
extra_por_equipamento: false, // Default
funcao_profissional_id: professionalData.funcaoId,
funcoes_ids: professionalData.funcoesIds, // Pass multi-select IDs
observacao: professionalData.observacao,
qtd_estudio: parseInt(professionalData.qtdEstudios) || 0,
tem_estudio: professionalData.possuiEstudio === "sim",

View file

@ -71,6 +71,7 @@ export const TeamPage: React.FC = () => {
const initialFormState: CreateProfessionalDTO & { senha?: string; confirmarSenha?: string } = {
nome: "",
funcao_profissional_id: "",
funcoes_ids: [],
email: "",
senha: "",
confirmarSenha: "",
@ -236,6 +237,7 @@ export const TeamPage: React.FC = () => {
setFormData({
nome: professional.nome,
funcao_profissional_id: professional.funcao_profissional_id,
funcoes_ids: professional.functions?.map(f => f.id) || (professional.funcao_profissional_id ? [professional.funcao_profissional_id] : []),
email: professional.email || "",
senha: "", // Não editamos senha aqui
confirmarSenha: "",
@ -587,9 +589,10 @@ export const TeamPage: React.FC = () => {
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<RoleIcon size={16} className="mr-2 text-gray-400" />
{roleName}
<div className="text-sm text-gray-900">
{p.functions && p.functions.length > 0
? p.functions.map(f => f.nome).join(", ")
: getRoleName(p.funcao_profissional_id)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
@ -660,12 +663,38 @@ export const TeamPage: React.FC = () => {
<label className="block text-sm font-medium text-gray-700">Nome *</label>
<input required type="text" value={formData.nome} onChange={e => setFormData({ ...formData, nome: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Função *</label>
<select required value={formData.funcao_profissional_id} onChange={e => setFormData({ ...formData, funcao_profissional_id: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border">
<option value="">Selecione...</option>
{roles.map(r => <option key={r.id} value={r.id}>{r.nome}</option>)}
</select>
<div className="col-span-1 md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Funções *
</label>
<div className="flex flex-wrap gap-4 bg-gray-50 p-3 rounded border">
{roles.map(role => (
<label key={role.id} className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
value={role.id}
checked={formData.funcoes_ids?.includes(role.id)}
onChange={e => {
const checked = e.target.checked;
const currentIds = formData.funcoes_ids || [];
let newIds: string[] = [];
if (checked) {
newIds = [...currentIds, role.id];
} else {
newIds = currentIds.filter((id) => id !== role.id);
}
setFormData((prev) => ({
...prev,
funcoes_ids: newIds,
funcao_profissional_id: newIds.length > 0 ? newIds[0] : ""
}));
}}
className="w-4 h-4 text-brand-gold rounded border-gray-300 focus:ring-brand-gold"
/>
<span className="text-sm text-gray-700">{role.nome}</span>
</label>
))}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Email *</label>

View file

@ -141,6 +141,7 @@ export interface EventData {
qtdPontosFoto?: number; // Quantidade de pontos de foto necessários
qtdPontosDecorados?: number; // Quantidade de pontos decorados necessários
qtdPontosLed?: number; // Quantidade de pontos LED necessários
qtdPlataforma360?: number; // Quantidade de Plataforma 360
// Fields populated from backend joins (ListAgendas)
fot?: string; // Nome/Número da Turma (FOT)
@ -197,11 +198,14 @@ export interface Professional {
// Availability (kept for compatibility, though might not be in main payload)
availability?: { [date: string]: boolean };
functions?: { id: string; nome: string }[];
}
export interface CreateProfessionalDTO {
nome: string;
funcao_profissional_id: string;
funcao_profissional_id: string; // Keep for compatibility (primary function)
funcoes_ids?: string[]; // New multi-select
email?: string;
endereco?: string;
cidade?: string;