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:
parent
a1d5434414
commit
e78de535c1
31 changed files with 2813 additions and 1070 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
311
backend/internal/db/generated/financial_transactions.sql.go
Normal file
311
backend/internal/db/generated/financial_transactions.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`
|
||||
|
|
|
|||
18
backend/internal/db/migrations/002_multi_function.sql
Normal file
18
backend/internal/db/migrations/002_multi_function.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
41
backend/internal/db/queries/financial_transactions.sql
Normal file
41
backend/internal/db/queries/financial_transactions.sql
Normal 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;
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
|
||||
|
|
|
|||
295
backend/internal/finance/handler.go
Normal file
295
backend/internal/finance/handler.go
Normal 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)
|
||||
}
|
||||
111
backend/internal/finance/service.go
Normal file
111
backend/internal/finance/service.go
Normal 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,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue