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:
parent
21987d221e
commit
a4982e588e
15 changed files with 582 additions and 193 deletions
36
backend/cmd/db_fix/main.go
Normal file
36
backend/cmd/db_fix/main.go
Normal 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")
|
||||
}
|
||||
42
backend/cmd/debug_fot/main.go
Normal file
42
backend/cmd/debug_fot/main.go
Normal 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, ®iao, &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)
|
||||
}
|
||||
}
|
||||
|
|
@ -321,7 +321,7 @@ func (q *Queries) GetAgendaByFotDataTipo(ctx context.Context, arg GetAgendaByFot
|
|||
}
|
||||
|
||||
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
|
||||
JOIN agenda_profissionais ap ON p.id = ap.profissional_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"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -385,6 +386,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -940,7 +942,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, arg ListAgendasByUserPa
|
|||
|
||||
const listAvailableProfessionalsForDate = `-- name: ListAvailableProfessionalsForDate :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,
|
||||
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,
|
||||
f.nome as funcao_nome,
|
||||
dp.status as status_disponibilidade
|
||||
|
|
@ -978,6 +980,7 @@ type ListAvailableProfessionalsForDateRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -1023,6 +1026,7 @@ func (q *Queries) ListAvailableProfessionalsForDate(ctx context.Context, arg Lis
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
|
|||
|
|
@ -128,13 +128,13 @@ func (q *Queries) GetCadastroFotByFOT(ctx context.Context, arg GetCadastroFotByF
|
|||
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, c.regiao,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
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
|
||||
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,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
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
|
||||
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,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.regiao = $1
|
||||
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
|
||||
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,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.empresa_id = $1 AND c.regiao = $2
|
||||
ORDER BY c.fot DESC
|
||||
`
|
||||
|
|
@ -403,13 +403,13 @@ func (q *Queries) ListCadastroFotByEmpresa(ctx context.Context, arg ListCadastro
|
|||
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, c.regiao,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.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
|
||||
ORDER BY c.fot ASC
|
||||
LIMIT 10
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ type CadastroProfissionai struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
|
|||
|
|
@ -39,13 +39,13 @@ func (q *Queries) ClearProfessionalFunctions(ctx context.Context, profissionalID
|
|||
const createProfissional = `-- name: CreateProfissional :one
|
||||
INSERT INTO cadastro_profissionais (
|
||||
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,
|
||||
educacao_simpatia, desempenho_evento, disp_horario, media,
|
||||
tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao
|
||||
) VALUES (
|
||||
$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
|
||||
nome = EXCLUDED.nome,
|
||||
|
|
@ -55,6 +55,7 @@ ON CONFLICT (email) DO UPDATE SET
|
|||
banco = EXCLUDED.banco,
|
||||
agencia = EXCLUDED.agencia,
|
||||
conta_pix = EXCLUDED.conta_pix,
|
||||
conta = EXCLUDED.conta,
|
||||
carro_disponivel = EXCLUDED.carro_disponivel,
|
||||
tem_estudio = EXCLUDED.tem_estudio,
|
||||
qtd_estudio = EXCLUDED.qtd_estudio,
|
||||
|
|
@ -71,7 +72,7 @@ ON CONFLICT (email) DO UPDATE SET
|
|||
avatar_url = EXCLUDED.avatar_url,
|
||||
regiao = EXCLUDED.regiao,
|
||||
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 {
|
||||
|
|
@ -86,6 +87,7 @@ type CreateProfissionalParams struct {
|
|||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
|
||||
|
|
@ -117,6 +119,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
|
|||
arg.Banco,
|
||||
arg.Agencia,
|
||||
arg.ContaPix,
|
||||
arg.Conta,
|
||||
arg.CarroDisponivel,
|
||||
arg.TemEstudio,
|
||||
arg.QtdEstudio,
|
||||
|
|
@ -147,6 +150,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -195,7 +199,7 @@ func (q *Queries) DeleteProfissional(ctx context.Context, arg DeleteProfissional
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
|
||||
FROM profissionais_funcoes_junction pfj
|
||||
|
|
@ -224,6 +228,7 @@ type GetProfissionalByCPFRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -261,6 +266,7 @@ func (q *Queries) GetProfissionalByCPF(ctx context.Context, arg GetProfissionalB
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -286,7 +292,7 @@ func (q *Queries) GetProfissionalByCPF(ctx context.Context, arg GetProfissionalB
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
|
||||
FROM profissionais_funcoes_junction pfj
|
||||
|
|
@ -315,6 +321,7 @@ type GetProfissionalByIDRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -352,6 +359,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, arg GetProfissionalBy
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -377,7 +385,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, arg GetProfissionalBy
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
|
||||
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
|
||||
) as functions
|
||||
FROM cadastro_profissionais p
|
||||
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||
WHERE p.usuario_id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
|
|
@ -401,6 +410,7 @@ type GetProfissionalByUsuarioIDRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -420,6 +430,7 @@ type GetProfissionalByUsuarioIDRow struct {
|
|||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
UsuarioEmail pgtype.Text `json:"usuario_email"`
|
||||
Functions interface{} `json:"functions"`
|
||||
}
|
||||
|
||||
|
|
@ -438,6 +449,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -457,6 +469,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty
|
|||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
&i.Regiao,
|
||||
&i.UsuarioEmail,
|
||||
&i.Functions,
|
||||
)
|
||||
return i, err
|
||||
|
|
@ -477,7 +490,7 @@ func (q *Queries) LinkUserToProfessional(ctx context.Context, arg LinkUserToProf
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
|
||||
FROM profissionais_funcoes_junction pfj
|
||||
|
|
@ -503,6 +516,7 @@ type ListProfissionaisRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -547,6 +561,7 @@ func (q *Queries) ListProfissionais(ctx context.Context, regiao pgtype.Text) ([]
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -580,7 +595,7 @@ func (q *Queries) ListProfissionais(ctx context.Context, regiao pgtype.Text) ([]
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f.id, 'nome', f.nome))
|
||||
FROM profissionais_funcoes_junction pfj
|
||||
|
|
@ -611,6 +626,7 @@ type SearchProfissionaisRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -654,6 +670,7 @@ func (q *Queries) SearchProfissionais(ctx context.Context, arg SearchProfissiona
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -686,7 +703,7 @@ func (q *Queries) SearchProfissionais(ctx context.Context, arg SearchProfissiona
|
|||
}
|
||||
|
||||
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(
|
||||
(SELECT json_agg(json_build_object('id', f2.id, 'nome', f2.nome))
|
||||
FROM profissionais_funcoes_junction pfj2
|
||||
|
|
@ -728,6 +745,7 @@ type SearchProfissionaisByFunctionRow struct {
|
|||
CpfCnpjTitular pgtype.Text `json:"cpf_cnpj_titular"`
|
||||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
|
|
@ -771,6 +789,7 @@ func (q *Queries) SearchProfissionaisByFunction(ctx context.Context, arg SearchP
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
@ -815,24 +834,25 @@ SET
|
|||
banco = $9,
|
||||
agencia = $10,
|
||||
conta_pix = $11,
|
||||
carro_disponivel = $12,
|
||||
tem_estudio = $13,
|
||||
qtd_estudio = $14,
|
||||
tipo_cartao = $15,
|
||||
observacao = $16,
|
||||
qual_tec = $17,
|
||||
educacao_simpatia = $18,
|
||||
desempenho_evento = $19,
|
||||
disp_horario = $20,
|
||||
media = $21,
|
||||
tabela_free = $22,
|
||||
extra_por_equipamento = $23,
|
||||
equipamentos = $24,
|
||||
avatar_url = $25,
|
||||
email = $26,
|
||||
conta = $12,
|
||||
carro_disponivel = $13,
|
||||
tem_estudio = $14,
|
||||
qtd_estudio = $15,
|
||||
tipo_cartao = $16,
|
||||
observacao = $17,
|
||||
qual_tec = $18,
|
||||
educacao_simpatia = $19,
|
||||
desempenho_evento = $20,
|
||||
disp_horario = $21,
|
||||
media = $22,
|
||||
tabela_free = $23,
|
||||
extra_por_equipamento = $24,
|
||||
equipamentos = $25,
|
||||
avatar_url = $26,
|
||||
email = $27,
|
||||
atualizado_em = NOW()
|
||||
WHERE id = $1 AND regiao = $27
|
||||
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
|
||||
WHERE id = $1 AND regiao = $28
|
||||
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 {
|
||||
|
|
@ -847,6 +867,7 @@ type UpdateProfissionalParams struct {
|
|||
Banco pgtype.Text `json:"banco"`
|
||||
Agencia pgtype.Text `json:"agencia"`
|
||||
ContaPix pgtype.Text `json:"conta_pix"`
|
||||
Conta pgtype.Text `json:"conta"`
|
||||
CarroDisponivel pgtype.Bool `json:"carro_disponivel"`
|
||||
TemEstudio pgtype.Bool `json:"tem_estudio"`
|
||||
QtdEstudio pgtype.Int4 `json:"qtd_estudio"`
|
||||
|
|
@ -878,6 +899,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
|
|||
arg.Banco,
|
||||
arg.Agencia,
|
||||
arg.ContaPix,
|
||||
arg.Conta,
|
||||
arg.CarroDisponivel,
|
||||
arg.TemEstudio,
|
||||
arg.QtdEstudio,
|
||||
|
|
@ -908,6 +930,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional
|
|||
&i.CpfCnpjTitular,
|
||||
&i.Banco,
|
||||
&i.Agencia,
|
||||
&i.Conta,
|
||||
&i.ContaPix,
|
||||
&i.CarroDisponivel,
|
||||
&i.TemEstudio,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20);
|
||||
|
|
@ -21,39 +21,39 @@ RETURNING *;
|
|||
-- name: ListCadastroFot :many
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.regiao = @regiao
|
||||
ORDER BY c.fot DESC;
|
||||
|
||||
-- name: ListCadastroFotByEmpresa :many
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.empresa_id = $1 AND c.regiao = @regiao
|
||||
ORDER BY c.fot DESC;
|
||||
|
||||
-- name: GetCadastroFotByID :one
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.id = $1 AND c.regiao = @regiao;
|
||||
|
||||
-- name: GetCadastroFotByFOT :one
|
||||
|
|
@ -81,13 +81,13 @@ DELETE FROM cadastro_fot WHERE id = $1 AND regiao = @regiao;
|
|||
-- name: GetCadastroFotByFotJoin :one
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.id
|
||||
LEFT JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.fot = $1 AND c.regiao = @regiao;
|
||||
|
||||
-- name: UpdateCadastroFotGastos :exec
|
||||
|
|
@ -99,13 +99,13 @@ WHERE id = $1 AND regiao = @regiao;
|
|||
-- name: SearchFot :many
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
COALESCE(e.nome, '') as empresa_nome,
|
||||
COALESCE(cur.nome, '') as curso_nome,
|
||||
COALESCE(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
|
||||
LEFT JOIN empresas e ON c.empresa_id = e.id
|
||||
LEFT JOIN cursos cur ON c.curso_id = cur.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
|
||||
ORDER BY c.fot ASC
|
||||
LIMIT 10;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
-- name: CreateProfissional :one
|
||||
INSERT INTO cadastro_profissionais (
|
||||
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,
|
||||
educacao_simpatia, desempenho_evento, disp_horario, media,
|
||||
tabela_free, extra_por_equipamento, equipamentos, email, avatar_url, regiao
|
||||
) VALUES (
|
||||
$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
|
||||
nome = EXCLUDED.nome,
|
||||
|
|
@ -17,6 +17,7 @@ ON CONFLICT (email) DO UPDATE SET
|
|||
banco = EXCLUDED.banco,
|
||||
agencia = EXCLUDED.agencia,
|
||||
conta_pix = EXCLUDED.conta_pix,
|
||||
conta = EXCLUDED.conta,
|
||||
carro_disponivel = EXCLUDED.carro_disponivel,
|
||||
tem_estudio = EXCLUDED.tem_estudio,
|
||||
qtd_estudio = EXCLUDED.qtd_estudio,
|
||||
|
|
@ -36,7 +37,7 @@ ON CONFLICT (email) DO UPDATE SET
|
|||
RETURNING *;
|
||||
|
||||
-- name: GetProfissionalByUsuarioID :one
|
||||
SELECT p.*,
|
||||
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
|
||||
|
|
@ -45,6 +46,7 @@ SELECT p.*,
|
|||
), '[]'::json
|
||||
) as functions
|
||||
FROM cadastro_profissionais p
|
||||
LEFT JOIN usuarios u ON p.usuario_id = u.id
|
||||
WHERE p.usuario_id = $1 LIMIT 1;
|
||||
|
||||
-- name: GetProfissionalByID :one
|
||||
|
|
@ -86,21 +88,22 @@ SET
|
|||
banco = $9,
|
||||
agencia = $10,
|
||||
conta_pix = $11,
|
||||
carro_disponivel = $12,
|
||||
tem_estudio = $13,
|
||||
qtd_estudio = $14,
|
||||
tipo_cartao = $15,
|
||||
observacao = $16,
|
||||
qual_tec = $17,
|
||||
educacao_simpatia = $18,
|
||||
desempenho_evento = $19,
|
||||
disp_horario = $20,
|
||||
media = $21,
|
||||
tabela_free = $22,
|
||||
extra_por_equipamento = $23,
|
||||
equipamentos = $24,
|
||||
avatar_url = $25,
|
||||
email = $26,
|
||||
conta = $12,
|
||||
carro_disponivel = $13,
|
||||
tem_estudio = $14,
|
||||
qtd_estudio = $15,
|
||||
tipo_cartao = $16,
|
||||
observacao = $17,
|
||||
qual_tec = $18,
|
||||
educacao_simpatia = $19,
|
||||
desempenho_evento = $20,
|
||||
disp_horario = $21,
|
||||
media = $22,
|
||||
tabela_free = $23,
|
||||
extra_por_equipamento = $24,
|
||||
equipamentos = $25,
|
||||
avatar_url = $26,
|
||||
email = $27,
|
||||
atualizado_em = NOW()
|
||||
WHERE id = $1 AND regiao = @regiao
|
||||
RETURNING *;
|
||||
|
|
@ -172,4 +175,3 @@ WHERE p.cpf_cnpj_titular = $1 AND p.regiao = @regiao LIMIT 1;
|
|||
|
||||
-- name: LinkUserToProfessional :exec
|
||||
UPDATE cadastro_profissionais SET usuario_id = $2 WHERE id = $1;
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS cadastro_profissionais (
|
|||
cpf_cnpj_titular VARCHAR(20) UNIQUE,
|
||||
banco VARCHAR(100),
|
||||
agencia VARCHAR(20),
|
||||
conta VARCHAR(20),
|
||||
conta_pix VARCHAR(120),
|
||||
carro_disponivel 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 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 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 usuarios
|
||||
SET regioes_permitidas = ARRAY['SP', 'MG']
|
||||
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
|
||||
WHEN duplicate_column THEN RAISE NOTICE 'column already exists';
|
||||
END $$;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package profissionais
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"photum-backend/internal/db/generated"
|
||||
|
|
@ -34,6 +35,7 @@ type ProfissionalResponse struct {
|
|||
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
|
||||
Banco *string `json:"banco"`
|
||||
Agencia *string `json:"agencia"`
|
||||
Conta *string `json:"conta"`
|
||||
ContaPix *string `json:"conta_pix"`
|
||||
CarroDisponivel *bool `json:"carro_disponivel"`
|
||||
TemEstudio *bool `json:"tem_estudio"`
|
||||
|
|
@ -74,6 +76,7 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
|
||||
Banco: fromPgText(v.Banco),
|
||||
Agencia: fromPgText(v.Agencia),
|
||||
Conta: fromPgText(v.Conta),
|
||||
ContaPix: fromPgText(v.ContaPix),
|
||||
CarroDisponivel: fromPgBool(v.CarroDisponivel),
|
||||
TemEstudio: fromPgBool(v.TemEstudio),
|
||||
|
|
@ -110,6 +113,7 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
|
||||
Banco: fromPgText(v.Banco),
|
||||
Agencia: fromPgText(v.Agencia),
|
||||
Conta: fromPgText(v.Conta),
|
||||
ContaPix: fromPgText(v.ContaPix),
|
||||
CarroDisponivel: fromPgBool(v.CarroDisponivel),
|
||||
TemEstudio: fromPgBool(v.TemEstudio),
|
||||
|
|
@ -142,6 +146,7 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
|
||||
Banco: fromPgText(v.Banco),
|
||||
Agencia: fromPgText(v.Agencia),
|
||||
Conta: fromPgText(v.Conta),
|
||||
ContaPix: fromPgText(v.ContaPix),
|
||||
CarroDisponivel: fromPgBool(v.CarroDisponivel),
|
||||
TemEstudio: fromPgBool(v.TemEstudio),
|
||||
|
|
@ -160,6 +165,10 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
AvatarURL: fromPgText(v.AvatarUrl),
|
||||
}
|
||||
case generated.GetProfissionalByUsuarioIDRow:
|
||||
email := fromPgText(v.Email)
|
||||
if email == nil {
|
||||
email = fromPgText(v.UsuarioEmail)
|
||||
}
|
||||
return ProfissionalResponse{
|
||||
ID: uuid.UUID(v.ID.Bytes).String(),
|
||||
UsuarioID: uuid.UUID(v.UsuarioID.Bytes).String(),
|
||||
|
|
@ -174,6 +183,7 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
CpfCnpjTitular: fromPgText(v.CpfCnpjTitular),
|
||||
Banco: fromPgText(v.Banco),
|
||||
Agencia: fromPgText(v.Agencia),
|
||||
Conta: fromPgText(v.Conta),
|
||||
ContaPix: fromPgText(v.ContaPix),
|
||||
CarroDisponivel: fromPgBool(v.CarroDisponivel),
|
||||
TemEstudio: fromPgBool(v.TemEstudio),
|
||||
|
|
@ -188,7 +198,7 @@ func toResponse(p interface{}) ProfissionalResponse {
|
|||
TabelaFree: fromPgText(v.TabelaFree),
|
||||
ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento),
|
||||
Equipamentos: fromPgText(v.Equipamentos),
|
||||
Email: fromPgText(v.Email),
|
||||
Email: email,
|
||||
AvatarURL: fromPgText(v.AvatarUrl),
|
||||
}
|
||||
default:
|
||||
|
|
@ -268,6 +278,7 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
fmt.Printf("[DEBUG] Create Input: %+v\n", input)
|
||||
|
||||
userID, exists := c.Get("userID")
|
||||
if !exists {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type CreateProfissionalInput struct {
|
|||
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
|
||||
Banco *string `json:"banco"`
|
||||
Agencia *string `json:"agencia"`
|
||||
Conta *string `json:"conta"`
|
||||
ContaPix *string `json:"conta_pix"`
|
||||
CarroDisponivel *bool `json:"carro_disponivel"`
|
||||
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),
|
||||
Banco: mergeStr(input.Banco, existing.Banco),
|
||||
Agencia: mergeStr(input.Agencia, existing.Agencia),
|
||||
Conta: mergeStr(input.Conta, existing.Conta),
|
||||
ContaPix: mergeStr(input.ContaPix, existing.ContaPix),
|
||||
CarroDisponivel: mergeBool(input.CarroDisponivel, existing.CarroDisponivel),
|
||||
TemEstudio: mergeBool(input.TemEstudio, existing.TemEstudio),
|
||||
|
|
@ -177,8 +179,12 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
|
|||
if err != nil {
|
||||
return nil, errors.New("invalid funcao_profissional_id")
|
||||
}
|
||||
funcaoUUID = parsed
|
||||
funcaoValid = true
|
||||
if parsed == uuid.Nil {
|
||||
funcaoValid = false
|
||||
} else {
|
||||
funcaoUUID = parsed
|
||||
funcaoValid = true
|
||||
}
|
||||
} else {
|
||||
funcaoValid = false
|
||||
}
|
||||
|
|
@ -194,6 +200,7 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss
|
|||
CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
|
||||
Banco: toPgText(input.Banco),
|
||||
Agencia: toPgText(input.Agencia),
|
||||
Conta: toPgText(input.Conta),
|
||||
ContaPix: toPgText(input.ContaPix),
|
||||
CarroDisponivel: toPgBool(input.CarroDisponivel),
|
||||
TemEstudio: toPgBool(input.TemEstudio),
|
||||
|
|
@ -271,6 +278,7 @@ type UpdateProfissionalInput struct {
|
|||
CpfCnpjTitular *string `json:"cpf_cnpj_titular"`
|
||||
Banco *string `json:"banco"`
|
||||
Agencia *string `json:"agencia"`
|
||||
Conta *string `json:"conta"`
|
||||
ContaPix *string `json:"conta_pix"`
|
||||
CarroDisponivel *bool `json:"carro_disponivel"`
|
||||
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")
|
||||
}
|
||||
|
||||
funcaoValid := true
|
||||
if funcaoUUID == uuid.Nil {
|
||||
funcaoValid = false
|
||||
}
|
||||
|
||||
params := generated.UpdateProfissionalParams{
|
||||
ID: pgtype.UUID{Bytes: uuidVal, Valid: true},
|
||||
Nome: input.Nome,
|
||||
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: true},
|
||||
FuncaoProfissionalID: pgtype.UUID{Bytes: funcaoUUID, Valid: funcaoValid},
|
||||
Endereco: toPgText(input.Endereco),
|
||||
Cidade: toPgText(input.Cidade),
|
||||
Uf: toPgText(input.Uf),
|
||||
|
|
@ -311,6 +324,7 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona
|
|||
CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
|
||||
Banco: toPgText(input.Banco),
|
||||
Agencia: toPgText(input.Agencia),
|
||||
Conta: toPgText(input.Conta),
|
||||
ContaPix: toPgText(input.ContaPix),
|
||||
CarroDisponivel: toPgBool(input.CarroDisponivel),
|
||||
TemEstudio: toPgBool(input.TemEstudio),
|
||||
|
|
@ -507,6 +521,7 @@ func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput, r
|
|||
CpfCnpjTitular: input.CpfCnpjTitular,
|
||||
Banco: input.Banco,
|
||||
Agencia: input.Agencia,
|
||||
Conta: input.Conta,
|
||||
ContaPix: input.ContaPix,
|
||||
CarroDisponivel: input.CarroDisponivel,
|
||||
TemEstudio: input.TemEstudio,
|
||||
|
|
@ -575,6 +590,7 @@ func (s *Service) Import(ctx context.Context, items []CreateProfissionalInput, r
|
|||
CpfCnpjTitular: toPgText(input.CpfCnpjTitular),
|
||||
Banco: toPgText(input.Banco),
|
||||
Agencia: toPgText(input.Agencia),
|
||||
Conta: toPgText(input.Conta),
|
||||
ContaPix: toPgText(input.ContaPix),
|
||||
CarroDisponivel: toPgBool(input.CarroDisponivel),
|
||||
TemEstudio: toPgBool(input.TemEstudio),
|
||||
|
|
|
|||
|
|
@ -777,12 +777,15 @@ const AppContent: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<RegionProvider>
|
||||
<DataProvider>
|
||||
<Toaster position="top-right" />
|
||||
<AppContent />
|
||||
</DataProvider>
|
||||
</RegionProvider>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import {
|
||||
User, Mail, Phone, MapPin, DollarSign, Briefcase,
|
||||
Camera, FileText, Check, CreditCard, Save, ChevronRight
|
||||
Camera, FileText, Check, CreditCard, Save, ChevronRight, Loader2, X
|
||||
} from "lucide-react";
|
||||
import { Navbar } from "../components/Navbar";
|
||||
import { Button } from "../components/Button";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { getFunctions } from "../services/apiService";
|
||||
import { getFunctions, createProfessional, updateProfessional } from "../services/apiService";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
// --- Helper Components ---
|
||||
|
|
@ -23,6 +23,8 @@ interface InputFieldProps {
|
|||
required?: boolean;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
onBlur?: (e: any) => void;
|
||||
}
|
||||
|
||||
const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps) => (
|
||||
|
|
@ -35,7 +37,7 @@ const InputField = ({ label, icon: Icon, className, ...props }: InputFieldProps)
|
|||
</div>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -80,8 +82,11 @@ export const ProfilePage: React.FC = () => {
|
|||
const { user, token } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("personal");
|
||||
const [functions, setFunctions] = useState<any[]>([]);
|
||||
const [isNewProfile, setIsNewProfile] = useState(false);
|
||||
const [showInitModal, setShowInitModal] = useState(false);
|
||||
|
||||
// Form State
|
||||
const [formData, setFormData] = useState<any>({
|
||||
|
|
@ -90,11 +95,16 @@ export const ProfilePage: React.FC = () => {
|
|||
email: "",
|
||||
whatsapp: "",
|
||||
cpf_cnpj_titular: "",
|
||||
cep: "",
|
||||
rua: "",
|
||||
numero: "",
|
||||
bairro: "",
|
||||
endereco: "",
|
||||
cidade: "",
|
||||
uf: "",
|
||||
banco: "",
|
||||
agencia: "",
|
||||
conta: "",
|
||||
conta_pix: "",
|
||||
carro_disponivel: false,
|
||||
tem_estudio: false,
|
||||
|
|
@ -113,38 +123,56 @@ export const ProfilePage: React.FC = () => {
|
|||
try {
|
||||
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`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) toast.error("Perfil profissional não encontrado.");
|
||||
else throw new Error("Falha ao carregar perfil");
|
||||
return;
|
||||
if (response.status === 404) {
|
||||
// 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;
|
||||
}
|
||||
throw new Error("Falha ao carregar perfil");
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setFormData({
|
||||
...data,
|
||||
carro_disponivel: data.carro_disponivel || false,
|
||||
tem_estudio: data.tem_estudio || false,
|
||||
extra_por_equipamento: data.extra_por_equipamento || false,
|
||||
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) : []
|
||||
});
|
||||
|
||||
const funcsRes = await getFunctions();
|
||||
if (funcsRes.data) setFunctions(funcsRes.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Erro ao carregar dados");
|
||||
toast.error("Erro ao carregar dados do perfil.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [token]);
|
||||
}, [token, user]);
|
||||
|
||||
const handleChange = (field: string, value: any) => {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
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}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Erro ao salvar");
|
||||
toast.success("Perfil atualizado!");
|
||||
} catch (error) {
|
||||
// Payload preparation
|
||||
// For create/update, we need `funcao_profissional_id` (single) for backward compatibility optionally
|
||||
// But we primarily use `funcoes_ids`.
|
||||
// If `funcoes_ids` is empty, user needs to select at least one?
|
||||
// For now, let's just pick the first one as "primary" if backend requires it.
|
||||
// Backend create DTO has `funcao_profissional_id`.
|
||||
|
||||
const payload = {
|
||||
...formData,
|
||||
// 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);
|
||||
toast.error("Erro ao salvar alterações");
|
||||
toast.error(error.message || "Erro ao salvar alterações");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
|
|
@ -202,7 +336,7 @@ export const ProfilePage: React.FC = () => {
|
|||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
|
|
@ -225,20 +359,26 @@ export const ProfilePage: React.FC = () => {
|
|||
active={activeTab === 'address'}
|
||||
onClick={() => setActiveTab('address')}
|
||||
/>
|
||||
<SidebarItem
|
||||
id="bank"
|
||||
label="Dados Bancários"
|
||||
icon={CreditCard}
|
||||
active={activeTab === 'bank'}
|
||||
onClick={() => setActiveTab('bank')}
|
||||
/>
|
||||
<SidebarItem
|
||||
id="equipment"
|
||||
label="Profissional"
|
||||
icon={Camera}
|
||||
active={activeTab === 'equipment'}
|
||||
onClick={() => setActiveTab('equipment')}
|
||||
/>
|
||||
|
||||
{/* Hide for clients/event owners */}
|
||||
{user?.role !== "EVENT_OWNER" && (
|
||||
<>
|
||||
<SidebarItem
|
||||
id="bank"
|
||||
label="Dados Bancários"
|
||||
icon={CreditCard}
|
||||
active={activeTab === 'bank'}
|
||||
onClick={() => setActiveTab('bank')}
|
||||
/>
|
||||
<SidebarItem
|
||||
id="equipment"
|
||||
label="Profissional"
|
||||
icon={Camera}
|
||||
active={activeTab === 'equipment'}
|
||||
onClick={() => setActiveTab('equipment')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -246,8 +386,10 @@ export const ProfilePage: React.FC = () => {
|
|||
{/* Mobile Tabs */}
|
||||
<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="flex gap-2 min-w-max">
|
||||
{['personal', 'address', 'bank', 'equipment'].map(id => (
|
||||
<div className="flex gap-2 min-w-max">
|
||||
{['personal', 'address',
|
||||
...(user?.role !== "EVENT_OWNER" ? ['bank', 'equipment'] : [])
|
||||
].map(id => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => setActiveTab(id)}
|
||||
|
|
@ -296,10 +438,21 @@ export const ProfilePage: React.FC = () => {
|
|||
) : (
|
||||
<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>
|
||||
<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} />
|
||||
<input type="file" className="hidden" accept="image/*" disabled />
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarChange}
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -312,14 +465,14 @@ export const ProfilePage: React.FC = () => {
|
|||
<InputField
|
||||
label="Nome Completo"
|
||||
icon={User}
|
||||
value={formData.nome}
|
||||
value={formData.nome || ""}
|
||||
onChange={(e) => handleChange("nome", e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputField
|
||||
label="CPF/CNPJ"
|
||||
icon={FileText}
|
||||
value={formData.cpf_cnpj_titular}
|
||||
value={formData.cpf_cnpj_titular || ""}
|
||||
onChange={(e) => handleChange("cpf_cnpj_titular", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -334,37 +487,96 @@ export const ProfilePage: React.FC = () => {
|
|||
label="Email"
|
||||
icon={Mail}
|
||||
type="email"
|
||||
value={formData.email}
|
||||
value={formData.email || ""}
|
||||
onChange={(e) => handleChange("email", e.target.value)}
|
||||
required
|
||||
/>
|
||||
<InputField
|
||||
label="WhatsApp"
|
||||
icon={Phone}
|
||||
value={formData.whatsapp}
|
||||
value={formData.whatsapp || ""}
|
||||
onChange={(e) => handleChange("whatsapp", e.target.value)}
|
||||
/>
|
||||
<div className="md:col-span-2">
|
||||
<InputField
|
||||
label="Endereço"
|
||||
icon={MapPin}
|
||||
value={formData.endereco}
|
||||
onChange={(e) => handleChange("endereco", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
label="Cidade"
|
||||
icon={MapPin}
|
||||
value={formData.cidade}
|
||||
onChange={(e) => handleChange("cidade", e.target.value)}
|
||||
/>
|
||||
<InputField
|
||||
label="UF"
|
||||
icon={MapPin}
|
||||
value={formData.uf}
|
||||
onChange={(e) => handleChange("uf", e.target.value)}
|
||||
maxLength={2}
|
||||
/>
|
||||
|
||||
<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">
|
||||
<InputField
|
||||
label="Endereço (Rua e Bairro)"
|
||||
icon={MapPin}
|
||||
value={formData.endereco || ""}
|
||||
onChange={(e) => handleChange("endereco", e.target.value)}
|
||||
readOnly
|
||||
/>
|
||||
</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
|
||||
label="Cidade"
|
||||
icon={MapPin}
|
||||
value={formData.cidade || ""}
|
||||
readOnly
|
||||
/>
|
||||
<InputField
|
||||
label="UF"
|
||||
icon={MapPin}
|
||||
value={formData.uf || ""}
|
||||
readOnly
|
||||
maxLength={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -372,23 +584,30 @@ export const ProfilePage: React.FC = () => {
|
|||
{/* --- BANK --- */}
|
||||
{activeTab === "bank" && (
|
||||
<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
|
||||
label="Banco"
|
||||
icon={Briefcase}
|
||||
value={formData.banco}
|
||||
value={formData.banco || ""}
|
||||
onChange={(e) => handleChange("banco", e.target.value)}
|
||||
/>
|
||||
<InputField
|
||||
label="Agência"
|
||||
icon={Briefcase}
|
||||
value={formData.agencia}
|
||||
value={formData.agencia || ""}
|
||||
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
|
||||
label="Chave PIX"
|
||||
icon={DollarSign}
|
||||
value={formData.conta_pix}
|
||||
value={formData.conta_pix || ""}
|
||||
onChange={(e) => handleChange("conta_pix", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -430,11 +649,7 @@ export const ProfilePage: React.FC = () => {
|
|||
checked={formData.carro_disponivel}
|
||||
onChange={(checked) => handleChange("carro_disponivel", checked)}
|
||||
/>
|
||||
<Toggle
|
||||
label="Cobra extra por equipamento?"
|
||||
checked={formData.extra_por_equipamento}
|
||||
onChange={(checked) => handleChange("extra_por_equipamento", checked)}
|
||||
/>
|
||||
{/* REMOVED: Cobra extra por equipamento */}
|
||||
<Toggle
|
||||
label="Possui estúdio?"
|
||||
checked={formData.tem_estudio}
|
||||
|
|
@ -442,11 +657,11 @@ export const ProfilePage: React.FC = () => {
|
|||
/>
|
||||
{formData.tem_estudio && (
|
||||
<InputField
|
||||
label="Qtd. Estúdios"
|
||||
icon={Briefcase}
|
||||
type="number"
|
||||
value={formData.qtd_estudio}
|
||||
onChange={(e) => handleChange("qtd_estudio", parseInt(e.target.value))}
|
||||
label="Qtd. Estúdios"
|
||||
icon={Briefcase}
|
||||
type="number"
|
||||
value={formData.qtd_estudio}
|
||||
onChange={(e) => handleChange("qtd_estudio", parseInt(e.target.value))}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -454,7 +669,7 @@ export const ProfilePage: React.FC = () => {
|
|||
<InputField
|
||||
label="Tipo de Cartão de Memória"
|
||||
icon={Briefcase}
|
||||
value={formData.tipo_cartao}
|
||||
value={formData.tipo_cartao || ""}
|
||||
onChange={(e) => handleChange("tipo_cartao", e.target.value)}
|
||||
placeholder="Ex: SD, CF Express..."
|
||||
/>
|
||||
|
|
@ -474,7 +689,7 @@ export const ProfilePage: React.FC = () => {
|
|||
|
||||
<div className="pt-6 sm:hidden">
|
||||
<Button type="submit" disabled={isSaving} className="w-full">
|
||||
{isSaving ? "Salvando..." : "Salvar Alterações"}
|
||||
{isSaving ? "Salvando..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -484,6 +699,34 @@ export const ProfilePage: React.FC = () => {
|
|||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue