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
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,

View file

@ -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

View file

@ -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"`

View file

@ -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,

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
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;

View file

@ -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;

View file

@ -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 $$;

View file

@ -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 {

View file

@ -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),

View file

@ -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>

View file

@ -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>
);
};