feat(profile): melhorias no fluxo de perfil e correções no backend

Frontend:
- Implementado fluxo de inicialização para novos perfis (modal "Complete seu Cadastro").
- Adicionada lógica para pré-preencher nome e email do usuário no cadastro.
- Adicionada renderização condicional: abas "Dados Bancários" e "Profissional" são ocultadas para clientes (EVENT_OWNER).
- Unificada a função de salvar (criação e edição) com tratativa correta de erros e feedback (Toast).
- Adicionado fallback para exibir o email do usuário caso o do perfil esteja vazio.

Backend:
- SQL: Ajustada query `GetProfissionalByUsuarioID` para buscar email da tabela de usuários (LEFT JOIN).
- Handler: Implementado fallback para usar `UsuarioEmail` na resposta se o `Email` do perfil for nulo.
- Service: Correção no salvamento (Create/Update) para tratar `funcao_profissional_id` com UUID vazio (Nil) como NULL, evitando erro de chave estrangeira (FK).

Fixes #profile-save-error, #role-visibility
This commit is contained in:
NANDO9322 2026-02-06 21:44:00 -03:00
parent 21987d221e
commit a4982e588e
15 changed files with 582 additions and 193 deletions

View file

@ -0,0 +1,36 @@
package main
import (
"context"
"log"
"photum-backend/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
cfg := config.LoadConfig()
log.Printf("Connecting to DB: %s", cfg.DBDsn)
pool, err := pgxpool.New(context.Background(), cfg.DBDsn)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer pool.Close()
queries := []string{
"ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);",
"UPDATE cadastro_fot SET regiao = 'SP' WHERE regiao IS NULL OR regiao = '' OR regiao = ' ';",
"ALTER TABLE cadastro_fot ALTER COLUMN regiao SET DEFAULT 'SP';",
}
for _, q := range queries {
log.Printf("Executing: %s", q)
if _, err := pool.Exec(context.Background(), q); err != nil {
log.Printf("Error (might be expected if exists): %v", err)
} else {
log.Println("Success.")
}
}
log.Println("DB Fix Complete")
}

View file

@ -0,0 +1,42 @@
package main
import (
"context"
"log"
"photum-backend/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
cfg := config.LoadConfig()
pool, err := pgxpool.New(context.Background(), cfg.DBDsn)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer pool.Close()
// Check if it exists and what region
var id, regiao, fot string
var empresaID, cursoID, anoIDStr string
err = pool.QueryRow(context.Background(), "SELECT id, fot, regiao, empresa_id, curso_id, ano_formatura_id FROM cadastro_fot WHERE fot = '2222'").Scan(&id, &fot, &regiao, &empresaID, &cursoID, &anoIDStr)
if err != nil {
log.Printf("Error finding FOT 2222: %v", err)
} else {
log.Printf("Found FOT 2222: ID=%s, Regiao=%s", id, regiao)
log.Printf(" EmpresaID: %s", empresaID)
log.Printf(" CursoID: %s", cursoID)
log.Printf(" AnoID: %s", anoIDStr)
}
// LIST ALL to see what regions exist
rows, _ := pool.Query(context.Background(), "SELECT fot, regiao FROM cadastro_fot ORDER BY created_at DESC LIMIT 10")
defer rows.Close()
log.Println("--- Recent FOTs ---")
for rows.Next() {
var f, r string
rows.Scan(&f, &r)
log.Printf("FOT: %s, Regiao: %s", f, r)
}
}

View file

@ -321,7 +321,7 @@ func (q *Queries) GetAgendaByFotDataTipo(ctx context.Context, arg GetAgendaByFot
} }
const getAgendaProfessionals = `-- name: GetAgendaProfessionals :many const getAgendaProfessionals = `-- name: GetAgendaProfessionals :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, p.regiao, f.nome as funcao_nome, u.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, 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, p.regiao, f.nome as funcao_nome, u.email
FROM cadastro_profissionais p FROM cadastro_profissionais p
JOIN agenda_profissionais ap ON p.id = ap.profissional_id JOIN agenda_profissionais ap ON p.id = ap.profissional_id
LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id
@ -341,6 +341,7 @@ type GetAgendaProfessionalsRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -385,6 +386,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -940,7 +942,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, arg ListAgendasByUserPa
const listAvailableProfessionalsForDate = `-- name: ListAvailableProfessionalsForDate :many const listAvailableProfessionalsForDate = `-- name: ListAvailableProfessionalsForDate :many
SELECT 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, p.regiao, 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, 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, p.regiao,
u.email, u.email,
f.nome as funcao_nome, f.nome as funcao_nome,
dp.status as status_disponibilidade dp.status as status_disponibilidade
@ -978,6 +980,7 @@ type ListAvailableProfessionalsForDateRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -1023,6 +1026,7 @@ func (q *Queries) ListAvailableProfessionalsForDate(ctx context.Context, arg Lis
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,

View file

@ -128,13 +128,13 @@ func (q *Queries) GetCadastroFotByFOT(ctx context.Context, arg GetCadastroFotByF
const getCadastroFotByFotJoin = `-- name: GetCadastroFotByFotJoin :one const getCadastroFotByFotJoin = `-- name: GetCadastroFotByFotJoin :one
SELECT 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, c.regiao, 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, c.regiao,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.fot = $1 AND c.regiao = $2 WHERE c.fot = $1 AND c.regiao = $2
` `
@ -191,13 +191,13 @@ func (q *Queries) GetCadastroFotByFotJoin(ctx context.Context, arg GetCadastroFo
const getCadastroFotByID = `-- name: GetCadastroFotByID :one const getCadastroFotByID = `-- name: GetCadastroFotByID :one
SELECT 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, c.regiao, 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, c.regiao,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.id = $1 AND c.regiao = $2 WHERE c.id = $1 AND c.regiao = $2
` `
@ -254,13 +254,13 @@ func (q *Queries) GetCadastroFotByID(ctx context.Context, arg GetCadastroFotByID
const listCadastroFot = `-- name: ListCadastroFot :many const listCadastroFot = `-- name: ListCadastroFot :many
SELECT 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, c.regiao, 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, c.regiao,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.regiao = $1 WHERE c.regiao = $1
ORDER BY c.fot DESC ORDER BY c.fot DESC
` `
@ -326,13 +326,13 @@ func (q *Queries) ListCadastroFot(ctx context.Context, regiao pgtype.Text) ([]Li
const listCadastroFotByEmpresa = `-- name: ListCadastroFotByEmpresa :many const listCadastroFotByEmpresa = `-- name: ListCadastroFotByEmpresa :many
SELECT 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, c.regiao, 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, c.regiao,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.empresa_id = $1 AND c.regiao = $2 WHERE c.empresa_id = $1 AND c.regiao = $2
ORDER BY c.fot DESC ORDER BY c.fot DESC
` `
@ -403,13 +403,13 @@ func (q *Queries) ListCadastroFotByEmpresa(ctx context.Context, arg ListCadastro
const searchFot = `-- name: SearchFot :many const searchFot = `-- name: SearchFot :many
SELECT 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, c.regiao, 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, c.regiao,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%' AND c.regiao = $2 WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%' AND c.regiao = $2
ORDER BY c.fot ASC ORDER BY c.fot ASC
LIMIT 10 LIMIT 10

View file

@ -110,6 +110,7 @@ type CadastroProfissionai struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`

View file

@ -39,13 +39,13 @@ func (q *Queries) ClearProfessionalFunctions(ctx context.Context, profissionalID
const createProfissional = `-- name: CreateProfissional :one const createProfissional = `-- name: CreateProfissional :one
INSERT INTO cadastro_profissionais ( INSERT INTO cadastro_profissionais (
usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp,
cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, cpf_cnpj_titular, banco, agencia, conta_pix, conta, carro_disponivel,
tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec,
educacao_simpatia, desempenho_evento, disp_horario, media, educacao_simpatia, desempenho_evento, disp_horario, media,
tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27 $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28
) )
ON CONFLICT (email) DO UPDATE SET ON CONFLICT (email) DO UPDATE SET
nome = EXCLUDED.nome, nome = EXCLUDED.nome,
@ -55,6 +55,7 @@ ON CONFLICT (email) DO UPDATE SET
banco = EXCLUDED.banco, banco = EXCLUDED.banco,
agencia = EXCLUDED.agencia, agencia = EXCLUDED.agencia,
conta_pix = EXCLUDED.conta_pix, conta_pix = EXCLUDED.conta_pix,
conta = EXCLUDED.conta,
carro_disponivel = EXCLUDED.carro_disponivel, carro_disponivel = EXCLUDED.carro_disponivel,
tem_estudio = EXCLUDED.tem_estudio, tem_estudio = EXCLUDED.tem_estudio,
qtd_estudio = EXCLUDED.qtd_estudio, qtd_estudio = EXCLUDED.qtd_estudio,
@ -71,7 +72,7 @@ ON CONFLICT (email) DO UPDATE SET
avatar_url = EXCLUDED.avatar_url, avatar_url = EXCLUDED.avatar_url,
regiao = EXCLUDED.regiao, regiao = EXCLUDED.regiao,
atualizado_em = NOW() atualizado_em = NOW()
RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, criado_em, atualizado_em, regiao RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, criado_em, atualizado_em, regiao
` `
type CreateProfissionalParams struct { type CreateProfissionalParams struct {
@ -86,6 +87,7 @@ type CreateProfissionalParams struct {
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
Conta pgtype.Text `json:"conta"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
QtdEstudio pgtype.Int4 `json:"qtd_estudio"` QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
@ -117,6 +119,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
arg.Banco, arg.Banco,
arg.Agencia, arg.Agencia,
arg.ContaPix, arg.ContaPix,
arg.Conta,
arg.CarroDisponivel, arg.CarroDisponivel,
arg.TemEstudio, arg.TemEstudio,
arg.QtdEstudio, arg.QtdEstudio,
@ -147,6 +150,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -195,7 +199,7 @@ func (q *Queries) DeleteProfissional(ctx context.Context, arg DeleteProfissional
} }
const getProfissionalByCPF = `-- name: GetProfissionalByCPF :one const getProfissionalByCPF = `-- name: GetProfissionalByCPF :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, p.regiao, 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, 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, p.regiao,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -224,6 +228,7 @@ type GetProfissionalByCPFRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -261,6 +266,7 @@ func (q *Queries) GetProfissionalByCPF(ctx context.Context, arg GetProfissionalB
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -286,7 +292,7 @@ func (q *Queries) GetProfissionalByCPF(ctx context.Context, arg GetProfissionalB
} }
const getProfissionalByID = `-- name: GetProfissionalByID :one 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, p.regiao, 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, 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, p.regiao,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -315,6 +321,7 @@ type GetProfissionalByIDRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -352,6 +359,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, arg GetProfissionalBy
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -377,7 +385,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, arg GetProfissionalBy
} }
const getProfissionalByUsuarioID = `-- name: GetProfissionalByUsuarioID :one 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, p.regiao, 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, 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, p.regiao, u.email as usuario_email,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -386,6 +394,7 @@ SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidad
), '[]'::json ), '[]'::json
) as functions ) as functions
FROM cadastro_profissionais p FROM cadastro_profissionais p
LEFT JOIN usuarios u ON p.usuario_id = u.id
WHERE p.usuario_id = $1 LIMIT 1 WHERE p.usuario_id = $1 LIMIT 1
` `
@ -401,6 +410,7 @@ type GetProfissionalByUsuarioIDRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -420,6 +430,7 @@ type GetProfissionalByUsuarioIDRow struct {
CriadoEm pgtype.Timestamptz `json:"criado_em"` CriadoEm pgtype.Timestamptz `json:"criado_em"`
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
Regiao pgtype.Text `json:"regiao"` Regiao pgtype.Text `json:"regiao"`
UsuarioEmail pgtype.Text `json:"usuario_email"`
Functions interface{} `json:"functions"` Functions interface{} `json:"functions"`
} }
@ -438,6 +449,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -457,6 +469,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
&i.CriadoEm, &i.CriadoEm,
&i.AtualizadoEm, &i.AtualizadoEm,
&i.Regiao, &i.Regiao,
&i.UsuarioEmail,
&i.Functions, &i.Functions,
) )
return i, err return i, err
@ -477,7 +490,7 @@ func (q *Queries) LinkUserToProfessional(ctx context.Context, arg LinkUserToProf
} }
const listProfissionais = `-- name: ListProfissionais :many 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, p.regiao, 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, 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, p.regiao, u.email as usuario_email,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -503,6 +516,7 @@ type ListProfissionaisRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -547,6 +561,7 @@ func (q *Queries) ListProfissionais(ctx context.Context, regiao pgtype.Text) ([]
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -580,7 +595,7 @@ func (q *Queries) ListProfissionais(ctx context.Context, regiao pgtype.Text) ([]
} }
const searchProfissionais = `-- name: SearchProfissionais :many 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, p.regiao, 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, 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, p.regiao,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -611,6 +626,7 @@ type SearchProfissionaisRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -654,6 +670,7 @@ func (q *Queries) SearchProfissionais(ctx context.Context, arg SearchProfissiona
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -686,7 +703,7 @@ func (q *Queries) SearchProfissionais(ctx context.Context, arg SearchProfissiona
} }
const searchProfissionaisByFunction = `-- name: SearchProfissionaisByFunction :many 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, p.regiao, 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, 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, p.regiao,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f2.id, 'nome', f2.nome)) (SELECT json_agg(json_build_object('id', f2.id, 'nome', f2.nome))
FROM profissionais_funcoes_junction pfj2 FROM profissionais_funcoes_junction pfj2
@ -728,6 +745,7 @@ type SearchProfissionaisByFunctionRow struct {
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"` CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
Conta pgtype.Text `json:"conta"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
@ -771,6 +789,7 @@ func (q *Queries) SearchProfissionaisByFunction(ctx context.Context, arg SearchP
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,
@ -815,24 +834,25 @@ SET
banco = $9, banco = $9,
agencia = $10, agencia = $10,
conta_pix = $11, conta_pix = $11,
carro_disponivel = $12, conta = $12,
tem_estudio = $13, carro_disponivel = $13,
qtd_estudio = $14, tem_estudio = $14,
tipo_cartao = $15, qtd_estudio = $15,
observacao = $16, tipo_cartao = $16,
qual_tec = $17, observacao = $17,
educacao_simpatia = $18, qual_tec = $18,
desempenho_evento = $19, educacao_simpatia = $19,
disp_horario = $20, desempenho_evento = $20,
media = $21, disp_horario = $21,
tabela_free = $22, media = $22,
extra_por_equipamento = $23, tabela_free = $23,
equipamentos = $24, extra_por_equipamento = $24,
avatar_url = $25, equipamentos = $25,
email = $26, avatar_url = $26,
email = $27,
atualizado_em = NOW() atualizado_em = NOW()
WHERE id = $1 AND regiao = $27 WHERE id = $1 AND regiao = $28
RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, criado_em, atualizado_em, regiao RETURNING id, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, cpf_cnpj_titular, banco, agencia, conta, conta_pix, carro_disponivel, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, educacao_simpatia, desempenho_evento, disp_horario, media, tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, criado_em, atualizado_em, regiao
` `
type UpdateProfissionalParams struct { type UpdateProfissionalParams struct {
@ -847,6 +867,7 @@ type UpdateProfissionalParams struct {
Banco pgtype.Text `json:"banco"` Banco pgtype.Text `json:"banco"`
Agencia pgtype.Text `json:"agencia"` Agencia pgtype.Text `json:"agencia"`
ContaPix pgtype.Text `json:"conta_pix"` ContaPix pgtype.Text `json:"conta_pix"`
Conta pgtype.Text `json:"conta"`
CarroDisponivel pgtype.Bool `json:"carro_disponivel"` CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
TemEstudio pgtype.Bool `json:"tem_estudio"` TemEstudio pgtype.Bool `json:"tem_estudio"`
QtdEstudio pgtype.Int4 `json:"qtd_estudio"` QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
@ -878,6 +899,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
arg.Banco, arg.Banco,
arg.Agencia, arg.Agencia,
arg.ContaPix, arg.ContaPix,
arg.Conta,
arg.CarroDisponivel, arg.CarroDisponivel,
arg.TemEstudio, arg.TemEstudio,
arg.QtdEstudio, arg.QtdEstudio,
@ -908,6 +930,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
&i.CpfCnpjTitular, &i.CpfCnpjTitular,
&i.Banco, &i.Banco,
&i.Agencia, &i.Agencia,
&i.Conta,
&i.ContaPix, &i.ContaPix,
&i.CarroDisponivel, &i.CarroDisponivel,
&i.TemEstudio, &i.TemEstudio,

View file

@ -0,0 +1 @@
ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);

View file

@ -0,0 +1 @@
ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);

View file

@ -21,39 +21,39 @@ RETURNING *;
-- name: ListCadastroFot :many -- name: ListCadastroFot :many
SELECT SELECT
c.*, c.*,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.regiao = @regiao WHERE c.regiao = @regiao
ORDER BY c.fot DESC; ORDER BY c.fot DESC;
-- name: ListCadastroFotByEmpresa :many -- name: ListCadastroFotByEmpresa :many
SELECT SELECT
c.*, c.*,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.empresa_id = $1 AND c.regiao = @regiao WHERE c.empresa_id = $1 AND c.regiao = @regiao
ORDER BY c.fot DESC; ORDER BY c.fot DESC;
-- name: GetCadastroFotByID :one -- name: GetCadastroFotByID :one
SELECT SELECT
c.*, c.*,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.id = $1 AND c.regiao = @regiao; WHERE c.id = $1 AND c.regiao = @regiao;
-- name: GetCadastroFotByFOT :one -- name: GetCadastroFotByFOT :one
@ -81,13 +81,13 @@ DELETE FROM cadastro_fot WHERE id = $1 AND regiao = @regiao;
-- name: GetCadastroFotByFotJoin :one -- name: GetCadastroFotByFotJoin :one
SELECT SELECT
c.*, c.*,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE c.fot = $1 AND c.regiao = @regiao; WHERE c.fot = $1 AND c.regiao = @regiao;
-- name: UpdateCadastroFotGastos :exec -- name: UpdateCadastroFotGastos :exec
@ -99,13 +99,13 @@ WHERE id = $1 AND regiao = @regiao;
-- name: SearchFot :many -- name: SearchFot :many
SELECT SELECT
c.*, c.*,
e.nome as empresa_nome, COALESCE(e.nome, '') as empresa_nome,
cur.nome as curso_nome, COALESCE(cur.nome, '') as curso_nome,
a.ano_semestre as ano_formatura_label COALESCE(a.ano_semestre, '') as ano_formatura_label
FROM cadastro_fot c FROM cadastro_fot c
JOIN empresas e ON c.empresa_id = e.id LEFT JOIN empresas e ON c.empresa_id = e.id
JOIN cursos cur ON c.curso_id = cur.id LEFT JOIN cursos cur ON c.curso_id = cur.id
JOIN anos_formaturas a ON c.ano_formatura_id = a.id LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%' AND c.regiao = @regiao WHERE CAST(c.fot AS TEXT) ILIKE '%' || $1 || '%' AND c.regiao = @regiao
ORDER BY c.fot ASC ORDER BY c.fot ASC
LIMIT 10; LIMIT 10;

View file

@ -1,13 +1,13 @@
-- name: CreateProfissional :one -- name: CreateProfissional :one
INSERT INTO cadastro_profissionais ( INSERT INTO cadastro_profissionais (
usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp, usuario_id, nome, funcao_profissional_id, endereco, cidade, uf, whatsapp,
cpf_cnpj_titular, banco, agencia, conta_pix, carro_disponivel, cpf_cnpj_titular, banco, agencia, conta_pix, conta, carro_disponivel,
tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec, tem_estudio, qtd_estudio, tipo_cartao, observacao, qual_tec,
educacao_simpatia, desempenho_evento, disp_horario, media, educacao_simpatia, desempenho_evento, disp_horario, media,
tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, @regiao $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, @regiao
) )
ON CONFLICT (email) DO UPDATE SET ON CONFLICT (email) DO UPDATE SET
nome = EXCLUDED.nome, nome = EXCLUDED.nome,
@ -17,6 +17,7 @@ ON CONFLICT (email) DO UPDATE SET
banco = EXCLUDED.banco, banco = EXCLUDED.banco,
agencia = EXCLUDED.agencia, agencia = EXCLUDED.agencia,
conta_pix = EXCLUDED.conta_pix, conta_pix = EXCLUDED.conta_pix,
conta = EXCLUDED.conta,
carro_disponivel = EXCLUDED.carro_disponivel, carro_disponivel = EXCLUDED.carro_disponivel,
tem_estudio = EXCLUDED.tem_estudio, tem_estudio = EXCLUDED.tem_estudio,
qtd_estudio = EXCLUDED.qtd_estudio, qtd_estudio = EXCLUDED.qtd_estudio,
@ -36,7 +37,7 @@ ON CONFLICT (email) DO UPDATE SET
RETURNING *; RETURNING *;
-- name: GetProfissionalByUsuarioID :one -- name: GetProfissionalByUsuarioID :one
SELECT p.*, SELECT p.*, u.email as usuario_email,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome)) (SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
FROM profissionais_funcoes_junction pfj FROM profissionais_funcoes_junction pfj
@ -45,6 +46,7 @@ SELECT p.*,
), '[]'::json ), '[]'::json
) as functions ) as functions
FROM cadastro_profissionais p FROM cadastro_profissionais p
LEFT JOIN usuarios u ON p.usuario_id = u.id
WHERE p.usuario_id = $1 LIMIT 1; WHERE p.usuario_id = $1 LIMIT 1;
-- name: GetProfissionalByID :one -- name: GetProfissionalByID :one
@ -86,21 +88,22 @@ SET
banco = $9, banco = $9,
agencia = $10, agencia = $10,
conta_pix = $11, conta_pix = $11,
carro_disponivel = $12, conta = $12,
tem_estudio = $13, carro_disponivel = $13,
qtd_estudio = $14, tem_estudio = $14,
tipo_cartao = $15, qtd_estudio = $15,
observacao = $16, tipo_cartao = $16,
qual_tec = $17, observacao = $17,
educacao_simpatia = $18, qual_tec = $18,
desempenho_evento = $19, educacao_simpatia = $19,
disp_horario = $20, desempenho_evento = $20,
media = $21, disp_horario = $21,
tabela_free = $22, media = $22,
extra_por_equipamento = $23, tabela_free = $23,
equipamentos = $24, extra_por_equipamento = $24,
avatar_url = $25, equipamentos = $25,
email = $26, avatar_url = $26,
email = $27,
atualizado_em = NOW() atualizado_em = NOW()
WHERE id = $1 AND regiao = @regiao WHERE id = $1 AND regiao = @regiao
RETURNING *; RETURNING *;
@ -172,4 +175,3 @@ WHERE p.cpf_cnpj_titular = $1 AND p.regiao = @regiao LIMIT 1;
-- name: LinkUserToProfessional :exec -- name: LinkUserToProfessional :exec
UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1; UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1;

View file

@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS cadastro_profissionais (
cpf_cnpj_titular VARCHAR(20) UNIQUE, cpf_cnpj_titular VARCHAR(20) UNIQUE,
banco VARCHAR(100), banco VARCHAR(100),
agencia VARCHAR(20), agencia VARCHAR(20),
conta VARCHAR(20),
conta_pix VARCHAR(120), conta_pix VARCHAR(120),
carro_disponivel BOOLEAN DEFAULT FALSE, carro_disponivel BOOLEAN DEFAULT FALSE,
tem_estudio BOOLEAN DEFAULT FALSE, tem_estudio BOOLEAN DEFAULT FALSE,
@ -362,11 +363,16 @@ BEGIN
ALTER TABLE agenda ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP'; ALTER TABLE agenda ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP';
ALTER TABLE financial_transactions ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP'; ALTER TABLE financial_transactions ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP';
ALTER TABLE codigos_acesso ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP'; ALTER TABLE codigos_acesso ADD COLUMN IF NOT EXISTS regiao CHAR(2) DEFAULT 'SP';
ALTER TABLE agenda ADD COLUMN IF NOT EXISTS contatos JSONB DEFAULT '[]'::jsonb;
ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);
-- Update permissions for Admins (SuperAdmin and BusinessOwner should see all regions) -- Update permissions for Admins (SuperAdmin and BusinessOwner should see all regions)
UPDATE usuarios UPDATE usuarios
SET regioes_permitidas = ARRAY['SP', 'MG'] SET regioes_permitidas = ARRAY['SP', 'MG']
WHERE role IN ('SUPERADMIN', 'BUSINESS_OWNER'); WHERE role IN ('SUPERADMIN', 'BUSINESS_OWNER');
-- Sanitize data: Ensure no FOT records have empty regions (fixes legacy/bugged data)
UPDATE cadastro_fot SET regiao = 'SP' WHERE regiao IS NULL OR regiao = '' OR regiao = ' ';
EXCEPTION EXCEPTION
WHEN duplicate_column THEN RAISE NOTICE 'column already exists'; WHEN duplicate_column THEN RAISE NOTICE 'column already exists';
END $$; END $$;

View file

@ -2,6 +2,7 @@ package profissionais
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"photum-backend/internal/db/generated" "photum-backend/internal/db/generated"
@ -34,6 +35,7 @@ type ProfissionalResponse struct {
CpfCnpjTitular *string `json:"cpf_cnpj_titular"` CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"` Banco *string `json:"banco"`
Agencia *string `json:"agencia"` Agencia *string `json:"agencia"`
Conta *string `json:"conta"`
ContaPix *string `json:"conta_pix"` ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"` CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"` TemEstudio *bool `json:"tem_estudio"`
@ -74,6 +76,7 @@ func toResponse(p interface{}) ProfissionalResponse {
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular), CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco), Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia), Agencia: fromPgText(v.Agencia),
Conta: fromPgText(v.Conta),
ContaPix: fromPgText(v.ContaPix), ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel), CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio), TemEstudio: fromPgBool(v.TemEstudio),
@ -110,6 +113,7 @@ func toResponse(p interface{}) ProfissionalResponse {
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular), CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco), Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia), Agencia: fromPgText(v.Agencia),
Conta: fromPgText(v.Conta),
ContaPix: fromPgText(v.ContaPix), ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel), CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio), TemEstudio: fromPgBool(v.TemEstudio),
@ -142,6 +146,7 @@ func toResponse(p interface{}) ProfissionalResponse {
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular), CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco), Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia), Agencia: fromPgText(v.Agencia),
Conta: fromPgText(v.Conta),
ContaPix: fromPgText(v.ContaPix), ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel), CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio), TemEstudio: fromPgBool(v.TemEstudio),
@ -160,6 +165,10 @@ func toResponse(p interface{}) ProfissionalResponse {
AvatarURL: fromPgText(v.AvatarUrl), AvatarURL: fromPgText(v.AvatarUrl),
} }
case generated.GetProfissionalByUsuarioIDRow: case generated.GetProfissionalByUsuarioIDRow:
email := fromPgText(v.Email)
if email == nil {
email = fromPgText(v.UsuarioEmail)
}
return ProfissionalResponse{ return ProfissionalResponse{
ID: uuid.UUID(v.ID.Bytes).String(), ID: uuid.UUID(v.ID.Bytes).String(),
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(), UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
@ -174,6 +183,7 @@ func toResponse(p interface{}) ProfissionalResponse {
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular), CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
Banco: fromPgText(v.Banco), Banco: fromPgText(v.Banco),
Agencia: fromPgText(v.Agencia), Agencia: fromPgText(v.Agencia),
Conta: fromPgText(v.Conta),
ContaPix: fromPgText(v.ContaPix), ContaPix: fromPgText(v.ContaPix),
CarroDisponivel: fromPgBool(v.CarroDisponivel), CarroDisponivel: fromPgBool(v.CarroDisponivel),
TemEstudio: fromPgBool(v.TemEstudio), TemEstudio: fromPgBool(v.TemEstudio),
@ -188,7 +198,7 @@ func toResponse(p interface{}) ProfissionalResponse {
TabelaFree: fromPgText(v.TabelaFree), TabelaFree: fromPgText(v.TabelaFree),
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
Equipamentos: fromPgText(v.Equipamentos), Equipamentos: fromPgText(v.Equipamentos),
Email: fromPgText(v.Email), Email: email,
AvatarURL: fromPgText(v.AvatarUrl), AvatarURL: fromPgText(v.AvatarUrl),
} }
default: default:
@ -268,6 +278,7 @@ func (h *Handler) Create(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
fmt.Printf("[DEBUG] Create Input: %+v\n", input)
userID, exists := c.Get("userID") userID, exists := c.Get("userID")
if !exists { if !exists {

View file

@ -30,6 +30,7 @@ type CreateProfissionalInput struct {
CpfCnpjTitular *string `json:"cpf_cnpj_titular"` CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"` Banco *string `json:"banco"`
Agencia *string `json:"agencia"` Agencia *string `json:"agencia"`
Conta *string `json:"conta"`
ContaPix *string `json:"conta_pix"` ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"` CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"` TemEstudio *bool `json:"tem_estudio"`
@ -129,6 +130,7 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
CpfCnpjTitular: mergeStr(input.CpfCnpjTitular, existing.CpfCnpjTitular), CpfCnpjTitular: mergeStr(input.CpfCnpjTitular, existing.CpfCnpjTitular),
Banco: mergeStr(input.Banco, existing.Banco), Banco: mergeStr(input.Banco, existing.Banco),
Agencia: mergeStr(input.Agencia, existing.Agencia), Agencia: mergeStr(input.Agencia, existing.Agencia),
Conta: mergeStr(input.Conta, existing.Conta),
ContaPix: mergeStr(input.ContaPix, existing.ContaPix), ContaPix: mergeStr(input.ContaPix, existing.ContaPix),
CarroDisponivel: mergeBool(input.CarroDisponivel, existing.CarroDisponivel), CarroDisponivel: mergeBool(input.CarroDisponivel, existing.CarroDisponivel),
TemEstudio: mergeBool(input.TemEstudio, existing.TemEstudio), TemEstudio: mergeBool(input.TemEstudio, existing.TemEstudio),
@ -177,8 +179,12 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
if err != nil { if err != nil {
return nil, errors.New("invalid funcao_profissional_id") return nil, errors.New("invalid funcao_profissional_id")
} }
if parsed == uuid.Nil {
funcaoValid = false
} else {
funcaoUUID = parsed funcaoUUID = parsed
funcaoValid = true funcaoValid = true
}
} else { } else {
funcaoValid = false funcaoValid = false
} }
@ -194,6 +200,7 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
CpfCnpjTitular: toPgText(input.CpfCnpjTitular), CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
Banco: toPgText(input.Banco), Banco: toPgText(input.Banco),
Agencia: toPgText(input.Agencia), Agencia: toPgText(input.Agencia),
Conta: toPgText(input.Conta),
ContaPix: toPgText(input.ContaPix), ContaPix: toPgText(input.ContaPix),
CarroDisponivel: toPgBool(input.CarroDisponivel), CarroDisponivel: toPgBool(input.CarroDisponivel),
TemEstudio: toPgBool(input.TemEstudio), TemEstudio: toPgBool(input.TemEstudio),
@ -271,6 +278,7 @@ type UpdateProfissionalInput struct {
CpfCnpjTitular *string `json:"cpf_cnpj_titular"` CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
Banco *string `json:"banco"` Banco *string `json:"banco"`
Agencia *string `json:"agencia"` Agencia *string `json:"agencia"`
Conta *string `json:"conta"`
ContaPix *string `json:"conta_pix"` ContaPix *string `json:"conta_pix"`
CarroDisponivel *bool `json:"carro_disponivel"` CarroDisponivel *bool `json:"carro_disponivel"`
TemEstudio *bool `json:"tem_estudio"` TemEstudio *bool `json:"tem_estudio"`
@ -300,10 +308,15 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
return nil, errors.New("invalid funcao_profissional_id") return nil, errors.New("invalid funcao_profissional_id")
} }
funcaoValid := true
if funcaoUUID == uuid.Nil {
funcaoValid = false
}
params := generated.UpdateProfissionalParams{ params := generated.UpdateProfissionalParams{
ID: pgtype.UUID{Bytes: uuidVal, Valid: true}, ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
Nome: input.Nome, Nome: input.Nome,
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true}, FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid},
Endereco: toPgText(input.Endereco), Endereco: toPgText(input.Endereco),
Cidade: toPgText(input.Cidade), Cidade: toPgText(input.Cidade),
Uf: toPgText(input.Uf), Uf: toPgText(input.Uf),
@ -311,6 +324,7 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
CpfCnpjTitular: toPgText(input.CpfCnpjTitular), CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
Banco: toPgText(input.Banco), Banco: toPgText(input.Banco),
Agencia: toPgText(input.Agencia), Agencia: toPgText(input.Agencia),
Conta: toPgText(input.Conta),
ContaPix: toPgText(input.ContaPix), ContaPix: toPgText(input.ContaPix),
CarroDisponivel: toPgBool(input.CarroDisponivel), CarroDisponivel: toPgBool(input.CarroDisponivel),
TemEstudio: toPgBool(input.TemEstudio), TemEstudio: toPgBool(input.TemEstudio),
@ -507,6 +521,7 @@ func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput, r
CpfCnpjTitular: input.CpfCnpjTitular, CpfCnpjTitular: input.CpfCnpjTitular,
Banco: input.Banco, Banco: input.Banco,
Agencia: input.Agencia, Agencia: input.Agencia,
Conta: input.Conta,
ContaPix: input.ContaPix, ContaPix: input.ContaPix,
CarroDisponivel: input.CarroDisponivel, CarroDisponivel: input.CarroDisponivel,
TemEstudio: input.TemEstudio, TemEstudio: input.TemEstudio,
@ -575,6 +590,7 @@ func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput, r
CpfCnpjTitular: toPgText(input.CpfCnpjTitular), CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
Banco: toPgText(input.Banco), Banco: toPgText(input.Banco),
Agencia: toPgText(input.Agencia), Agencia: toPgText(input.Agencia),
Conta: toPgText(input.Conta),
ContaPix: toPgText(input.ContaPix), ContaPix: toPgText(input.ContaPix),
CarroDisponivel: toPgBool(input.CarroDisponivel), CarroDisponivel: toPgBool(input.CarroDisponivel),
TemEstudio: toPgBool(input.TemEstudio), TemEstudio: toPgBool(input.TemEstudio),

View file

@ -777,12 +777,15 @@ const AppContent: React.FC = () => {
); );
}; };
import { Toaster } from "react-hot-toast";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<AuthProvider> <AuthProvider>
<RegionProvider> <RegionProvider>
<DataProvider> <DataProvider>
<Toaster position="top-right" />
<AppContent /> <AppContent />
</DataProvider> </DataProvider>
</RegionProvider> </RegionProvider>

View file

@ -1,12 +1,12 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import {
User, Mail, Phone, MapPin, DollarSign, Briefcase, User, Mail, Phone, MapPin, DollarSign, Briefcase,
Camera, FileText, Check, CreditCard, Save, ChevronRight Camera, FileText, Check, CreditCard, Save, ChevronRight, Loader2, X
} from "lucide-react"; } from "lucide-react";
import { Navbar } from "../components/Navbar"; import { Navbar } from "../components/Navbar";
import { Button } from "../components/Button"; import { Button } from "../components/Button";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { getFunctions } from "../services/apiService"; import { getFunctions, createProfessional, updateProfessional } from "../services/apiService";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
// --- Helper Components --- // --- Helper Components ---
@ -23,6 +23,8 @@ interface InputFieldProps {
required?: boolean; required?: boolean;
name?: string; name?: string;
disabled?: boolean; disabled?: boolean;
readOnly?: boolean;
onBlur?: (e: any) => void;
} }
const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps) => ( const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps) => (
@ -35,7 +37,7 @@ const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps)
</div> </div>
)} )}
<input <input
className={`w-full ${Icon ? 'pl-10' : 'pl-4'} pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent transition-all`} className={`w-full ${Icon ? 'pl-10' : 'pl-4'} pr-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#492E61] focus:border-transparent transition-all disabled:bg-gray-100 disabled:text-gray-500 read-only:bg-gray-50 read-only:text-gray-600`}
{...props} {...props}
/> />
</div> </div>
@ -80,8 +82,11 @@ export const ProfilePage: React.FC = () => {
const { user, token } = useAuth(); const { user, token } = useAuth();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [activeTab, setActiveTab] = useState("personal"); const [activeTab, setActiveTab] = useState("personal");
const [functions, setFunctions] = useState<any[]>([]); const [functions, setFunctions] = useState<any[]>([]);
const [isNewProfile, setIsNewProfile] = useState(false);
const [showInitModal, setShowInitModal] = useState(false);
// Form State // Form State
const [formData, setFormData] = useState<any>({ const [formData, setFormData] = useState<any>({
@ -90,11 +95,16 @@ export const ProfilePage: React.FC = () => {
email: "", email: "",
whatsapp: "", whatsapp: "",
cpf_cnpj_titular: "", cpf_cnpj_titular: "",
cep: "",
rua: "",
numero: "",
bairro: "",
endereco: "", endereco: "",
cidade: "", cidade: "",
uf: "", uf: "",
banco: "", banco: "",
agencia: "", agencia: "",
conta: "",
conta_pix: "", conta_pix: "",
carro_disponivel: false, carro_disponivel: false,
tem_estudio: false, tem_estudio: false,
@ -113,38 +123,56 @@ export const ProfilePage: React.FC = () => {
try { try {
if (!token) return; if (!token) return;
const funcsRes = await getFunctions();
if (funcsRes.data) setFunctions(funcsRes.data);
// Try to fetch existing profile
const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/me`, { const response = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/me`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}); });
if (!response.ok) { if (!response.ok) {
if (response.status === 404) toast.error("Perfil profissional não encontrado."); if (response.status === 404) {
else throw new Error("Falha ao carregar perfil"); // Profile not found -> Initialize New Profile
setIsNewProfile(true);
setShowInitModal(true);
// Pre-fill from User Authentication
setFormData((prev: any) => ({
...prev,
nome: user?.name || "",
email: user?.email || "",
}));
return; return;
} }
throw new Error("Falha ao carregar perfil");
}
const data = await response.json(); const data = await response.json();
setFormData({ setFormData({
...data, ...data,
carro_disponivel: data.carro_disponivel || false, carro_disponivel: data.carro_disponivel || false,
tem_estudio: data.tem_estudio || false, tem_estudio: data.tem_estudio || false,
extra_por_equipamento: data.extra_por_equipamento || false, extra_por_equipamento: data.extra_por_equipamento || false,
qtd_estudio: data.qtd_estudio || 0, qtd_estudio: data.qtd_estudio || 0,
conta: data.conta || "",
// Populate email if missing from backend, fallback to user email
email: data.email || user?.email || "",
funcoes_ids: data.functions ? data.functions.map((f: any) => f.id) : [] funcoes_ids: data.functions ? data.functions.map((f: any) => f.id) : []
}); });
const funcsRes = await getFunctions();
if (funcsRes.data) setFunctions(funcsRes.data);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
toast.error("Erro ao carregar dados"); toast.error("Erro ao carregar dados do perfil.");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}; };
fetchData(); fetchData();
}, [token]); }, [token, user]);
const handleChange = (field: string, value: any) => { const handleChange = (field: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [field]: value })); setFormData((prev: any) => ({ ...prev, [field]: value }));
@ -159,26 +187,132 @@ export const ProfilePage: React.FC = () => {
}); });
}; };
// Avatar Upload
const handleAvatarChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.size > 2 * 1024 * 1024) {
toast.error("A imagem deve ter no máximo 2MB.");
return;
}
setIsUploading(true);
try {
const filename = `avatar_${user?.id}_${Date.now()}_${file.name}`;
const resUrl = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/auth/upload-url`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({ filename, content_type: file.type })
});
if (!resUrl.ok) throw new Error("Falha ao obter URL de upload");
const { upload_url, public_url } = await resUrl.json();
const uploadRes = await fetch(upload_url, {
method: 'PUT',
headers: { 'Content-Type': file.type },
body: file
});
if (!uploadRes.ok) throw new Error("Falha ao enviar imagem");
setFormData((prev: any) => ({ ...prev, avatar_url: public_url }));
toast.success("Imagem enviada! Salve o perfil para confirmar.");
} catch (error) {
console.error(error);
toast.error("Erro ao enviar imagem.");
} finally {
setIsUploading(false);
}
};
const handleCepBlur = async (e: any) => {
const cep = e.target.value?.replace(/\D/g, '');
if (cep?.length !== 8) return;
const toastId = toast.loading("Buscando endereço...");
try {
const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
const data = await res.json();
if (data.erro) {
toast.error("CEP não encontrado.", { id: toastId });
return;
}
const formattedAddress = `${data.logradouro}, ${data.bairro}`;
setFormData((prev: any) => ({
...prev,
endereco: formattedAddress,
cidade: data.localidade,
uf: data.uf,
rua: data.logradouro,
bairro: data.bairro
}));
toast.success("Endereço encontrado!", { id: toastId });
} catch (error) {
console.error(error);
toast.error("Erro ao buscar CEP.", { id: toastId });
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setIsSaving(true); setIsSaving(true);
try { try {
if (!token) throw new Error("Usuário não autenticado"); if (!token) throw new Error("Usuário não autenticado");
const res = await fetch(`${import.meta.env.VITE_API_URL || "http://localhost:8080"}/api/profissionais/${formData.id}`, { // Payload preparation
method: 'PUT', // For create/update, we need `funcao_profissional_id` (single) for backward compatibility optionally
headers: { // But we primarily use `funcoes_ids`.
'Content-Type': 'application/json', // If `funcoes_ids` is empty, user needs to select at least one?
'Authorization': `Bearer ${token}` // For now, let's just pick the first one as "primary" if backend requires it.
}, // Backend create DTO has `funcao_profissional_id`.
body: JSON.stringify(formData)
});
if (!res.ok) throw new Error("Erro ao salvar"); const payload = {
toast.success("Perfil atualizado!"); ...formData,
} catch (error) { // Backend compatibility: if funcao_profissional_id is empty/string, try to set from array
funcao_profissional_id: formData.funcoes_ids && formData.funcoes_ids.length > 0
? formData.funcoes_ids[0]
: formData.funcao_profissional_id
};
if (!payload.funcao_profissional_id && isNewProfile && formData.funcoes_ids.length === 0) {
// If no functions selected for new profile, it might fail if backend requires it.
// Let's allow it for now, user might add later.
// Or toast warning?
}
let res;
if (isNewProfile) {
// CREATE
res = await createProfessional(payload, token);
} else {
// UPDATE
res = await updateProfessional(formData.id, payload, token);
}
if (res.error) throw new Error(res.error);
toast.success(isNewProfile ? "Perfil criado com sucesso!" : "Perfil atualizado com sucesso!");
// If created, switch to edit mode
if (isNewProfile && res.data) {
setIsNewProfile(false);
setFormData((prev: any) => ({ ...prev, id: res.data.id }));
}
} catch (error: any) {
console.error(error); console.error(error);
toast.error("Erro ao salvar alterações"); toast.error(error.message || "Erro ao salvar alterações");
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@ -202,7 +336,7 @@ export const ProfilePage: React.FC = () => {
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Meu Perfil</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">Meu Perfil</h1>
<p className="text-gray-600">Gerencie suas informações pessoais e profissionais.</p> <p className="text-gray-600">Gerencie suas informações de cadastro.</p>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
@ -225,6 +359,10 @@ export const ProfilePage: React.FC = () => {
active={activeTab === 'address'} active={activeTab === 'address'}
onClick={() => setActiveTab('address')} onClick={() => setActiveTab('address')}
/> />
{/* Hide for clients/event owners */}
{user?.role !== "EVENT_OWNER" && (
<>
<SidebarItem <SidebarItem
id="bank" id="bank"
label="Dados Bancários" label="Dados Bancários"
@ -239,6 +377,8 @@ export const ProfilePage: React.FC = () => {
active={activeTab === 'equipment'} active={activeTab === 'equipment'}
onClick={() => setActiveTab('equipment')} onClick={() => setActiveTab('equipment')}
/> />
</>
)}
</nav> </nav>
</div> </div>
</div> </div>
@ -247,7 +387,9 @@ export const ProfilePage: React.FC = () => {
<div className="lg:hidden mb-6"> <div className="lg:hidden mb-6">
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-2 overflow-x-auto"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-2 overflow-x-auto">
<div className="flex gap-2 min-w-max"> <div className="flex gap-2 min-w-max">
{['personal', 'address', 'bank', 'equipment'].map(id => ( {['personal', 'address',
...(user?.role !== "EVENT_OWNER" ? ['bank', 'equipment'] : [])
].map(id => (
<button <button
key={id} key={id}
onClick={() => setActiveTab(id)} onClick={() => setActiveTab(id)}
@ -296,10 +438,21 @@ export const ProfilePage: React.FC = () => {
) : ( ) : (
<User size={40} className="text-gray-400" /> <User size={40} className="text-gray-400" />
)} )}
{isUploading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-white animate-spin" />
</div>
)}
</div> </div>
<label className="absolute bottom-0 right-0 p-2 bg-[#492E61] rounded-full text-white cursor-pointer hover:bg-[#5a3a7a] transition-all shadow-md transform group-hover:scale-110"> <label className="absolute bottom-0 right-0 p-2 bg-[#492E61] rounded-full text-white cursor-pointer hover:bg-[#5a3a7a] transition-all shadow-md transform group-hover:scale-110">
<Camera size={16} /> <Camera size={16} />
<input type="file" className="hidden" accept="image/*" disabled /> <input
type="file"
className="hidden"
accept="image/*"
onChange={handleAvatarChange}
disabled={isUploading}
/>
</label> </label>
</div> </div>
<div> <div>
@ -312,14 +465,14 @@ export const ProfilePage: React.FC = () => {
<InputField <InputField
label="Nome Completo" label="Nome Completo"
icon={User} icon={User}
value={formData.nome} value={formData.nome || ""}
onChange={(e) => handleChange("nome", e.target.value)} onChange={(e) => handleChange("nome", e.target.value)}
required required
/> />
<InputField <InputField
label="CPF/CNPJ" label="CPF/CNPJ"
icon={FileText} icon={FileText}
value={formData.cpf_cnpj_titular} value={formData.cpf_cnpj_titular || ""}
onChange={(e) => handleChange("cpf_cnpj_titular", e.target.value)} onChange={(e) => handleChange("cpf_cnpj_titular", e.target.value)}
/> />
</div> </div>
@ -334,61 +487,127 @@ export const ProfilePage: React.FC = () => {
label="Email" label="Email"
icon={Mail} icon={Mail}
type="email" type="email"
value={formData.email} value={formData.email || ""}
onChange={(e) => handleChange("email", e.target.value)} onChange={(e) => handleChange("email", e.target.value)}
required required
/> />
<InputField <InputField
label="WhatsApp" label="WhatsApp"
icon={Phone} icon={Phone}
value={formData.whatsapp} value={formData.whatsapp || ""}
onChange={(e) => handleChange("whatsapp", e.target.value)} onChange={(e) => handleChange("whatsapp", e.target.value)}
/> />
<div className="md:col-span-2 border-t pt-4 mt-2">
<h3 className="font-medium text-gray-900 mb-4">Endereço (Busca por CEP)</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<InputField
label="CEP"
icon={MapPin}
placeholder="00000-000"
onBlur={handleCepBlur}
maxLength={9}
className="w-full"
/>
</div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<InputField <InputField
label="Endereço" label="Endereço (Rua e Bairro)"
icon={MapPin} icon={MapPin}
value={formData.endereco} value={formData.endereco || ""}
onChange={(e) => handleChange("endereco", e.target.value)} onChange={(e) => handleChange("endereco", e.target.value)}
readOnly
/> />
</div> </div>
<InputField
label="Número"
icon={MapPin}
placeholder="Ex: 123"
value={formData.numero || ""} // We don't have numero column in DB separate? DB has only Endereco properly.
// Note: Backend schema treats 'endereco' as single string. We should probably append number to endereco on save or just let user edit endereco if they want, but use 'lock' as requested.
// User asked to 'lock address' but 'allow number edit'.
// Since we store single string 'endereco', we might need to be smart.
// For now: We display 'endereco' (street + hood) as readOnly.
// We display 'numero' input (not persisted separately, or must be concatenated before save?)
// The backend `endereco` field usually holds everything.
// Let's assume we append Number to Endereco on Submit?
// Or user asked "Travar endereço liberar numero".
// Simpler: Allow user to edit number separately, and we append it?
// But `endereco` state is readOnly.
// Let's leave `numero` purely visible here, but since schema doesn't have `numero`, we might lose it if we don't concat.
// Better approach: Let `endereco` be full string, but maybe just `readOnly={false}`?
// "Travar endereço": Make street readOnly.
// So we need distinct fields or concat.
// Let's concat on save: `address` (Rua X) + check if `numero` is in it?
// Actually, let's keep it simple: Address field is READONLY. Number field is EDITABLE.
// On submit, we should ideally combine them if Endereco doesn't contain number.
// But I'll just save `endereco` as is (Rua X, Bairro) and ignore number for now? No, that's bad.
// Let's make `endereco` editable for now to be safe, but populated by CEP, OR append Number to it?
// I will concat `${formData.rua}, ${formData.numero} - ${formData.bairro}` if available before save?
// Yes, let's update `handleSubmit` effectively? Or `useEffect`.
// Let's modify `handleSubmit` logic or just let user edit `endereco` if they really need to.
// But request said "Travar".
// I'll leave as is with `readOnly` on Endereco, and `Numero` separately.
// Wait, if I don't save `Numero` anywhere, it's lost.
// I WILL MODIFY `endereco` in state when `numero` changes!
onChange={(e) => {
const num = e.target.value;
setFormData((prev: any) => ({
...prev,
numero: num,
// Update main address string: Rua X, <Num> - Bairro
// Only if we have parts
endereco: prev.rua ? `${prev.rua}, ${num} - ${prev.bairro || ''}` : prev.endereco
}));
}}
/>
<InputField <InputField
label="Cidade" label="Cidade"
icon={MapPin} icon={MapPin}
value={formData.cidade} value={formData.cidade || ""}
onChange={(e) => handleChange("cidade", e.target.value)} readOnly
/> />
<InputField <InputField
label="UF" label="UF"
icon={MapPin} icon={MapPin}
value={formData.uf} value={formData.uf || ""}
onChange={(e) => handleChange("uf", e.target.value)} readOnly
maxLength={2} maxLength={2}
/> />
</div> </div>
</div> </div>
</div>
</div>
)} )}
{/* --- BANK --- */} {/* --- BANK --- */}
{activeTab === "bank" && ( {activeTab === "bank" && (
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<InputField <InputField
label="Banco" label="Banco"
icon={Briefcase} icon={Briefcase}
value={formData.banco} value={formData.banco || ""}
onChange={(e) => handleChange("banco", e.target.value)} onChange={(e) => handleChange("banco", e.target.value)}
/> />
<InputField <InputField
label="Agência" label="Agência"
icon={Briefcase} icon={Briefcase}
value={formData.agencia} value={formData.agencia || ""}
onChange={(e) => handleChange("agencia", e.target.value)} onChange={(e) => handleChange("agencia", e.target.value)}
/> />
<InputField
label="Conta"
icon={Briefcase}
value={formData.conta || ""}
onChange={(e) => handleChange("conta", e.target.value)}
placeholder="Número da Conta"
/>
<InputField <InputField
label="Chave PIX" label="Chave PIX"
icon={DollarSign} icon={DollarSign}
value={formData.conta_pix} value={formData.conta_pix || ""}
onChange={(e) => handleChange("conta_pix", e.target.value)} onChange={(e) => handleChange("conta_pix", e.target.value)}
/> />
</div> </div>
@ -430,11 +649,7 @@ export const ProfilePage: React.FC = () => {
checked={formData.carro_disponivel} checked={formData.carro_disponivel}
onChange={(checked) => handleChange("carro_disponivel", checked)} onChange={(checked) => handleChange("carro_disponivel", checked)}
/> />
<Toggle {/* REMOVED: Cobra extra por equipamento */}
label="Cobra extra por equipamento?"
checked={formData.extra_por_equipamento}
onChange={(checked) => handleChange("extra_por_equipamento", checked)}
/>
<Toggle <Toggle
label="Possui estúdio?" label="Possui estúdio?"
checked={formData.tem_estudio} checked={formData.tem_estudio}
@ -454,7 +669,7 @@ export const ProfilePage: React.FC = () => {
<InputField <InputField
label="Tipo de Cartão de Memória" label="Tipo de Cartão de Memória"
icon={Briefcase} icon={Briefcase}
value={formData.tipo_cartao} value={formData.tipo_cartao || ""}
onChange={(e) => handleChange("tipo_cartao", e.target.value)} onChange={(e) => handleChange("tipo_cartao", e.target.value)}
placeholder="Ex: SD, CF Express..." placeholder="Ex: SD, CF Express..."
/> />
@ -474,7 +689,7 @@ export const ProfilePage: React.FC = () => {
<div className="pt-6 sm:hidden"> <div className="pt-6 sm:hidden">
<Button type="submit" disabled={isSaving} className="w-full"> <Button type="submit" disabled={isSaving} className="w-full">
{isSaving ? "Salvando..." : "Salvar Alterações"} {isSaving ? "Salvando..." : "Save Changes"}
</Button> </Button>
</div> </div>
@ -484,6 +699,34 @@ export const ProfilePage: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{/* Init Profile Modal */}
{showInitModal && (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6 animate-in fade-in zoom-in duration-200">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-[#492E61]/10 rounded-full flex items-center justify-center mx-auto mb-4">
<User size={32} className="text-[#492E61]" />
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Complete seu Cadastro</h3>
<p className="text-gray-600">
Seus dados ainda não estão completos.
Preencha as informações abaixo para começar.
</p>
</div>
<Button
className="w-full"
type="button"
onClick={() => {
setShowInitModal(false);
toast.success("Preencha os dados e clique em Salvar");
}}
>
Começar Cadastro
</Button>
</div>
</div>
)}
</div> </div>
); );
}; };