From a4982e588e592ef3ff017d178dc1f89f95c62cfc Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Fri, 6 Feb 2026 21:44:00 -0300 Subject: [PATCH] =?UTF-8?q?feat(profile):=20melhorias=20no=20fluxo=20de=20?= =?UTF-8?q?perfil=20e=20corre=C3=A7=C3=B5es=20no=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/cmd/db_fix/main.go | 36 ++ backend/cmd/debug_fot/main.go | 42 ++ backend/internal/db/generated/agenda.sql.go | 8 +- .../internal/db/generated/cadastro_fot.sql.go | 60 +-- backend/internal/db/generated/models.go | 1 + .../db/generated/profissionais.sql.go | 75 ++-- .../db/migrations/015_add_conta_column.up.sql | 1 + .../db/migrations/016_add_conta_column.up.sql | 1 + backend/internal/db/queries/cadastro_fot.sql | 60 +-- backend/internal/db/queries/profissionais.sql | 40 +- backend/internal/db/schema.sql | 6 + backend/internal/profissionais/handler.go | 13 +- backend/internal/profissionais/service.go | 22 +- frontend/App.tsx | 3 + frontend/pages/Profile.tsx | 407 ++++++++++++++---- 15 files changed, 582 insertions(+), 193 deletions(-) create mode 100644 backend/cmd/db_fix/main.go create mode 100644 backend/cmd/debug_fot/main.go create mode 100644 backend/internal/db/migrations/015_add_conta_column.up.sql create mode 100644 backend/internal/db/migrations/016_add_conta_column.up.sql diff --git a/backend/cmd/db_fix/main.go b/backend/cmd/db_fix/main.go new file mode 100644 index 0000000..d149022 --- /dev/null +++ b/backend/cmd/db_fix/main.go @@ -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") +} diff --git a/backend/cmd/debug_fot/main.go b/backend/cmd/debug_fot/main.go new file mode 100644 index 0000000..e438e0a --- /dev/null +++ b/backend/cmd/debug_fot/main.go @@ -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) + } +} diff --git a/backend/internal/db/generated/agenda.sql.go b/backend/internal/db/generated/agenda.sql.go index c007544..faae4e6 100644 --- a/backend/internal/db/generated/agenda.sql.go +++ b/backend/internal/db/generated/agenda.sql.go @@ -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, diff --git a/backend/internal/db/generated/cadastro_fot.sql.go b/backend/internal/db/generated/cadastro_fot.sql.go index 32badac..d858c60 100644 --- a/backend/internal/db/generated/cadastro_fot.sql.go +++ b/backend/internal/db/generated/cadastro_fot.sql.go @@ -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 diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index 95e8f21..290fb86 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -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"` diff --git a/backend/internal/db/generated/profissionais.sql.go b/backend/internal/db/generated/profissionais.sql.go index 8f6755c..b73d9d5 100644 --- a/backend/internal/db/generated/profissionais.sql.go +++ b/backend/internal/db/generated/profissionais.sql.go @@ -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, diff --git a/backend/internal/db/migrations/015_add_conta_column.up.sql b/backend/internal/db/migrations/015_add_conta_column.up.sql new file mode 100644 index 0000000..881428e --- /dev/null +++ b/backend/internal/db/migrations/015_add_conta_column.up.sql @@ -0,0 +1 @@ +ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20); diff --git a/backend/internal/db/migrations/016_add_conta_column.up.sql b/backend/internal/db/migrations/016_add_conta_column.up.sql new file mode 100644 index 0000000..881428e --- /dev/null +++ b/backend/internal/db/migrations/016_add_conta_column.up.sql @@ -0,0 +1 @@ +ALTER TABLE cadastro_profissionais ADD COLUMN IF NOT EXISTS conta VARCHAR(20); diff --git a/backend/internal/db/queries/cadastro_fot.sql b/backend/internal/db/queries/cadastro_fot.sql index e0d6cbf..b709929 100644 --- a/backend/internal/db/queries/cadastro_fot.sql +++ b/backend/internal/db/queries/cadastro_fot.sql @@ -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; diff --git a/backend/internal/db/queries/profissionais.sql b/backend/internal/db/queries/profissionais.sql index 25d6e22..1a069fb 100644 --- a/backend/internal/db/queries/profissionais.sql +++ b/backend/internal/db/queries/profissionais.sql @@ -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; - diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index ff15e19..2aea773 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -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 $$; diff --git a/backend/internal/profissionais/handler.go b/backend/internal/profissionais/handler.go index 840a319..bce5b38 100644 --- a/backend/internal/profissionais/handler.go +++ b/backend/internal/profissionais/handler.go @@ -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 { diff --git a/backend/internal/profissionais/service.go b/backend/internal/profissionais/service.go index cd34919..5635bd8 100644 --- a/backend/internal/profissionais/service.go +++ b/backend/internal/profissionais/service.go @@ -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), diff --git a/frontend/App.tsx b/frontend/App.tsx index 2a851b8..ec53953 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -777,12 +777,15 @@ const AppContent: React.FC = () => { ); }; +import { Toaster } from "react-hot-toast"; + function App() { return ( + diff --git a/frontend/pages/Profile.tsx b/frontend/pages/Profile.tsx index 13e7b39..e11aac3 100644 --- a/frontend/pages/Profile.tsx +++ b/frontend/pages/Profile.tsx @@ -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) )} @@ -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([]); + const [isNewProfile, setIsNewProfile] = useState(false); + const [showInitModal, setShowInitModal] = useState(false); // Form State const [formData, setFormData] = useState({ @@ -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) => { + 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 */}

Meu Perfil

-

Gerencie suas informações pessoais e profissionais.

+

Gerencie suas informações de cadastro.

@@ -225,20 +359,26 @@ export const ProfilePage: React.FC = () => { active={activeTab === 'address'} onClick={() => setActiveTab('address')} /> - setActiveTab('bank')} - /> - setActiveTab('equipment')} - /> + + {/* Hide for clients/event owners */} + {user?.role !== "EVENT_OWNER" && ( + <> + setActiveTab('bank')} + /> + setActiveTab('equipment')} + /> + + )}
@@ -246,8 +386,10 @@ export const ProfilePage: React.FC = () => { {/* Mobile Tabs */}
-
- {['personal', 'address', 'bank', 'equipment'].map(id => ( +
+ {['personal', 'address', + ...(user?.role !== "EVENT_OWNER" ? ['bank', 'equipment'] : []) + ].map(id => (
@@ -312,14 +465,14 @@ export const ProfilePage: React.FC = () => { handleChange("nome", e.target.value)} required /> handleChange("cpf_cnpj_titular", e.target.value)} />
@@ -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 /> handleChange("whatsapp", e.target.value)} /> -
- handleChange("endereco", e.target.value)} - /> -
- handleChange("cidade", e.target.value)} - /> - handleChange("uf", e.target.value)} - maxLength={2} - /> + +
+

Endereço (Busca por CEP)

+
+
+ +
+
+ handleChange("endereco", e.target.value)} + readOnly + /> +
+ { + const num = e.target.value; + setFormData((prev: any) => ({ + ...prev, + numero: num, + // Update main address string: Rua X, - Bairro + // Only if we have parts + endereco: prev.rua ? `${prev.rua}, ${num} - ${prev.bairro || ''}` : prev.endereco + })); + }} + /> + + +
+
)} @@ -372,23 +584,30 @@ export const ProfilePage: React.FC = () => { {/* --- BANK --- */} {activeTab === "bank" && (
-
+
handleChange("banco", e.target.value)} /> handleChange("agencia", e.target.value)} /> + handleChange("conta", e.target.value)} + placeholder="Número da Conta" + /> handleChange("conta_pix", e.target.value)} />
@@ -430,11 +649,7 @@ export const ProfilePage: React.FC = () => { checked={formData.carro_disponivel} onChange={(checked) => handleChange("carro_disponivel", checked)} /> - handleChange("extra_por_equipamento", checked)} - /> + {/* REMOVED: Cobra extra por equipamento */} { /> {formData.tem_estudio && ( 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 = () => { handleChange("tipo_cartao", e.target.value)} placeholder="Ex: SD, CF Express..." /> @@ -474,7 +689,7 @@ export const ProfilePage: React.FC = () => {
@@ -484,6 +699,34 @@ export const ProfilePage: React.FC = () => {
+ + {/* Init Profile Modal */} + {showInitModal && ( +
+
+
+
+ +
+

Complete seu Cadastro

+

+ Seus dados ainda não estão completos. + Preencha as informações abaixo para começar. +

+
+ +
+
+ )} ); };