From 90841596c1afe8d155f5b760ad0b8c1068419f7d Mon Sep 17 00:00:00 2001 From: NANDO9322 Date: Wed, 24 Dec 2025 15:03:08 -0300 Subject: [PATCH] =?UTF-8?q?feat(profissionais):=20melhorar=20a=20visualiza?= =?UTF-8?q?=C3=A7=C3=A3o=20de=20detalhes=20e=20persist=C3=AAncia=20dos=20p?= =?UTF-8?q?rofissionais=20-=20Adiciona=20a=20coluna=20`email`=20ao=20banco?= =?UTF-8?q?=20de=20dados=20para=20corrigir=20a=20persist=C3=AAncia=20do=20?= =?UTF-8?q?e-mail=20de=20contato.=20-=20Atualiza=20o=20`Team.tsx`=20para?= =?UTF-8?q?=20exibir=20todos=20os=20campos=20do=20profissional=20no=20moda?= =?UTF-8?q?l=20de=20detalhes=20(Dados=20Financeiros,=20Detalhamento=20de?= =?UTF-8?q?=20Avalia=C3=A7=C3=B5es).=20-=20Corrige=20o=20c=C3=A1lculo=20e?= =?UTF-8?q?=20a=20persist=C3=AAncia=20de=20`media`=20(ajuste=20para=20valo?= =?UTF-8?q?r=20nulo).=20-=20Implementa=20integra=C3=A7=C3=A3o=20com=20CEP?= =?UTF-8?q?=20para=20preenchimento=20autom=C3=A1tico=20de=20endere=C3=A7o.?= =?UTF-8?q?=20-=20Adiciona=20valida=C3=A7=C3=B5es=20para=20valores=20negat?= =?UTF-8?q?ivos=20e=20corrige=20problemas=20de=20layout.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/docs/docs.go | 9 + backend/docs/swagger.json | 9 + backend/docs/swagger.yaml | 6 + backend/internal/db/generated/agenda.sql.go | 16 +- backend/internal/db/generated/models.go | 1 + .../db/generated/profissionais.sql.go | 31 +- backend/internal/db/queries/profissionais.sql | 7 +- backend/internal/db/schema.sql | 1 + backend/internal/profissionais/handler.go | 9 +- backend/internal/profissionais/service.go | 9 +- frontend/pages/Team.tsx | 2243 ++++++----------- frontend/services/apiService.ts | 102 + frontend/types.ts | 74 +- 13 files changed, 997 insertions(+), 1520 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index d794857..b990cb4 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -2997,6 +2997,9 @@ const docTemplate = `{ "educacao_simpatia": { "type": "integer" }, + "email": { + "type": "string" + }, "endereco": { "type": "string" }, @@ -3047,6 +3050,9 @@ const docTemplate = `{ "agencia": { "type": "string" }, + "avatar_url": { + "type": "string" + }, "banco": { "type": "string" }, @@ -3161,6 +3167,9 @@ const docTemplate = `{ "educacao_simpatia": { "type": "integer" }, + "email": { + "type": "string" + }, "endereco": { "type": "string" }, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 636b7ea..341971f 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -2991,6 +2991,9 @@ "educacao_simpatia": { "type": "integer" }, + "email": { + "type": "string" + }, "endereco": { "type": "string" }, @@ -3041,6 +3044,9 @@ "agencia": { "type": "string" }, + "avatar_url": { + "type": "string" + }, "banco": { "type": "string" }, @@ -3155,6 +3161,9 @@ "educacao_simpatia": { "type": "integer" }, + "email": { + "type": "string" + }, "endereco": { "type": "string" }, diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 9993a91..3f47eea 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -274,6 +274,8 @@ definitions: type: integer educacao_simpatia: type: integer + email: + type: string endereco: type: string equipamentos: @@ -307,6 +309,8 @@ definitions: properties: agencia: type: string + avatar_url: + type: string banco: type: string carro_disponivel: @@ -383,6 +387,8 @@ definitions: type: integer educacao_simpatia: type: integer + email: + type: string endereco: type: string equipamentos: diff --git a/backend/internal/db/generated/agenda.sql.go b/backend/internal/db/generated/agenda.sql.go index bc0410f..2c30e21 100644 --- a/backend/internal/db/generated/agenda.sql.go +++ b/backend/internal/db/generated/agenda.sql.go @@ -198,7 +198,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error) } 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.avatar_url, p.criado_em, p.atualizado_em, 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_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email 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 @@ -232,11 +232,12 @@ type GetAgendaProfessionalsRow struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` FuncaoNome pgtype.Text `json:"funcao_nome"` - Email pgtype.Text `json:"email"` + Email_2 pgtype.Text `json:"email_2"` } func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UUID) ([]GetAgendaProfessionalsRow, error) { @@ -274,11 +275,12 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, &i.FuncaoNome, - &i.Email, + &i.Email_2, ); err != nil { return nil, err } @@ -543,7 +545,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([] 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.avatar_url, p.criado_em, p.atualizado_em, + p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, u.email, f.nome as funcao_nome, dp.status as status_disponibilidade @@ -589,10 +591,11 @@ type ListAvailableProfessionalsForDateRow struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` - Email string `json:"email"` + Email_2 string `json:"email_2"` FuncaoNome string `json:"funcao_nome"` StatusDisponibilidade string `json:"status_disponibilidade"` } @@ -632,10 +635,11 @@ func (q *Queries) ListAvailableProfessionalsForDate(ctx context.Context, data pg &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, - &i.Email, + &i.Email_2, &i.FuncaoNome, &i.StatusDisponibilidade, ); err != nil { diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index e88cbcf..b7a48c5 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -107,6 +107,7 @@ type CadastroProfissionai struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` diff --git a/backend/internal/db/generated/profissionais.sql.go b/backend/internal/db/generated/profissionais.sql.go index c958ffb..99a9cb7 100644 --- a/backend/internal/db/generated/profissionais.sql.go +++ b/backend/internal/db/generated/profissionais.sql.go @@ -17,11 +17,11 @@ INSERT INTO cadastro_profissionais ( 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, avatar_url + tabela_free, extra_por_equipamento, equipamentos, email, avatar_url ) 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 -) 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, avatar_url, criado_em, atualizado_em + $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26 +) 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 ` type CreateProfissionalParams struct { @@ -49,6 +49,7 @@ type CreateProfissionalParams struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` } @@ -78,6 +79,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional arg.TabelaFree, arg.ExtraPorEquipamento, arg.Equipamentos, + arg.Email, arg.AvatarUrl, ) var i CadastroProfissionai @@ -107,6 +109,7 @@ func (q *Queries) CreateProfissional(ctx context.Context, arg CreateProfissional &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, @@ -125,7 +128,7 @@ func (q *Queries) DeleteProfissional(ctx context.Context, id pgtype.UUID) error } const getProfissionalByID = `-- name: GetProfissionalByID :one -SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome +SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome FROM cadastro_profissionais p LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id WHERE p.id = $1 LIMIT 1 @@ -157,6 +160,7 @@ type GetProfissionalByIDRow struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` @@ -192,6 +196,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetP &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, @@ -201,7 +206,7 @@ func (q *Queries) GetProfissionalByID(ctx context.Context, id pgtype.UUID) (GetP } 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.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome +SELECT p.id, p.usuario_id, p.nome, p.funcao_profissional_id, p.endereco, p.cidade, p.uf, p.whatsapp, p.cpf_cnpj_titular, p.banco, p.agencia, p.conta_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome FROM cadastro_profissionais p LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id WHERE p.usuario_id = $1 LIMIT 1 @@ -233,6 +238,7 @@ type GetProfissionalByUsuarioIDRow struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` @@ -268,6 +274,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, @@ -277,7 +284,7 @@ func (q *Queries) GetProfissionalByUsuarioID(ctx context.Context, usuarioID pgty } 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.avatar_url, p.criado_em, p.atualizado_em, 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_pix, p.carro_disponivel, p.tem_estudio, p.qtd_estudio, p.tipo_cartao, p.observacao, p.qual_tec, p.educacao_simpatia, p.desempenho_evento, p.disp_horario, p.media, p.tabela_free, p.extra_por_equipamento, p.equipamentos, p.email, p.avatar_url, p.criado_em, p.atualizado_em, f.nome as funcao_nome, u.email as usuario_email FROM cadastro_profissionais p LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id LEFT JOIN usuarios u ON p.usuario_id = u.id @@ -310,11 +317,12 @@ type ListProfissionaisRow struct { TabelaFree pgtype.Text `json:"tabela_free"` ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` + Email pgtype.Text `json:"email"` AvatarUrl pgtype.Text `json:"avatar_url"` CriadoEm pgtype.Timestamptz `json:"criado_em"` AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"` FuncaoNome pgtype.Text `json:"funcao_nome"` - Email pgtype.Text `json:"email"` + UsuarioEmail pgtype.Text `json:"usuario_email"` } func (q *Queries) ListProfissionais(ctx context.Context) ([]ListProfissionaisRow, error) { @@ -352,11 +360,12 @@ func (q *Queries) ListProfissionais(ctx context.Context) ([]ListProfissionaisRow &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, &i.FuncaoNome, - &i.Email, + &i.UsuarioEmail, ); err != nil { return nil, err } @@ -395,9 +404,10 @@ SET extra_por_equipamento = $23, equipamentos = $24, avatar_url = $25, + email = $26, atualizado_em = NOW() WHERE id = $1 -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, avatar_url, criado_em, atualizado_em +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 ` type UpdateProfissionalParams struct { @@ -426,6 +436,7 @@ type UpdateProfissionalParams struct { ExtraPorEquipamento pgtype.Bool `json:"extra_por_equipamento"` Equipamentos pgtype.Text `json:"equipamentos"` AvatarUrl pgtype.Text `json:"avatar_url"` + Email pgtype.Text `json:"email"` } func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissionalParams) (CadastroProfissionai, error) { @@ -455,6 +466,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional arg.ExtraPorEquipamento, arg.Equipamentos, arg.AvatarUrl, + arg.Email, ) var i CadastroProfissionai err := row.Scan( @@ -483,6 +495,7 @@ func (q *Queries) UpdateProfissional(ctx context.Context, arg UpdateProfissional &i.TabelaFree, &i.ExtraPorEquipamento, &i.Equipamentos, + &i.Email, &i.AvatarUrl, &i.CriadoEm, &i.AtualizadoEm, diff --git a/backend/internal/db/queries/profissionais.sql b/backend/internal/db/queries/profissionais.sql index a21d5df..66282ff 100644 --- a/backend/internal/db/queries/profissionais.sql +++ b/backend/internal/db/queries/profissionais.sql @@ -4,10 +4,10 @@ INSERT INTO cadastro_profissionais ( 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, avatar_url + tabela_free, extra_por_equipamento, equipamentos, email, avatar_url ) 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 + $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26 ) RETURNING *; -- name: GetProfissionalByUsuarioID :one @@ -23,7 +23,7 @@ LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id WHERE p.id = $1 LIMIT 1; -- name: ListProfissionais :many -SELECT p.*, f.nome as funcao_nome, u.email +SELECT p.*, f.nome as funcao_nome, u.email as usuario_email FROM cadastro_profissionais p LEFT JOIN funcoes_profissionais f ON p.funcao_profissional_id = f.id LEFT JOIN usuarios u ON p.usuario_id = u.id @@ -56,6 +56,7 @@ SET extra_por_equipamento = $23, equipamentos = $24, avatar_url = $25, + email = $26, atualizado_em = NOW() WHERE id = $1 RETURNING *; diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 747ef01..606735e 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -52,6 +52,7 @@ CREATE TABLE IF NOT EXISTS cadastro_profissionais ( tabela_free VARCHAR(50), extra_por_equipamento BOOLEAN DEFAULT FALSE, equipamentos TEXT, + email VARCHAR(255), avatar_url VARCHAR(255), criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW() diff --git a/backend/internal/profissionais/handler.go b/backend/internal/profissionais/handler.go index fd6c19e..4a2eea8 100644 --- a/backend/internal/profissionais/handler.go +++ b/backend/internal/profissionais/handler.go @@ -46,7 +46,8 @@ type ProfissionalResponse struct { TabelaFree *string `json:"tabela_free"` ExtraPorEquipamento *bool `json:"extra_por_equipamento"` Equipamentos *string `json:"equipamentos"` - Email string `json:"email"` + Email *string `json:"email"` + AvatarURL *string `json:"avatar_url"` } func toResponse(p interface{}) ProfissionalResponse { @@ -84,6 +85,8 @@ func toResponse(p interface{}) ProfissionalResponse { TabelaFree: fromPgText(v.TabelaFree), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), Equipamentos: fromPgText(v.Equipamentos), + Email: fromPgText(v.Email), + AvatarURL: fromPgText(v.AvatarUrl), } case generated.ListProfissionaisRow: return ProfissionalResponse{ @@ -113,7 +116,8 @@ func toResponse(p interface{}) ProfissionalResponse { TabelaFree: fromPgText(v.TabelaFree), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), Equipamentos: fromPgText(v.Equipamentos), - Email: v.Email.String, + Email: fromPgText(v.Email), + AvatarURL: fromPgText(v.AvatarUrl), } case generated.GetProfissionalByIDRow: return ProfissionalResponse{ @@ -143,6 +147,7 @@ func toResponse(p interface{}) ProfissionalResponse { TabelaFree: fromPgText(v.TabelaFree), ExtraPorEquipamento: fromPgBool(v.ExtraPorEquipamento), Equipamentos: fromPgText(v.Equipamentos), + AvatarURL: fromPgText(v.AvatarUrl), } default: return ProfissionalResponse{} diff --git a/backend/internal/profissionais/service.go b/backend/internal/profissionais/service.go index 1eafe17..8d56f57 100644 --- a/backend/internal/profissionais/service.go +++ b/backend/internal/profissionais/service.go @@ -3,6 +3,7 @@ package profissionais import ( "context" "errors" + "fmt" "photum-backend/internal/db/generated" @@ -42,6 +43,7 @@ type CreateProfissionalInput struct { TabelaFree *string `json:"tabela_free"` ExtraPorEquipamento *bool `json:"extra_por_equipamento"` Equipamentos *string `json:"equipamentos"` + Email *string `json:"email"` AvatarURL *string `json:"avatar_url"` } @@ -89,6 +91,7 @@ func (s *Service) Create(ctx context.Context, userID string, input CreateProfiss TabelaFree: toPgText(input.TabelaFree), ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento), Equipamentos: toPgText(input.Equipamentos), + Email: toPgText(input.Email), AvatarUrl: toPgText(input.AvatarURL), } @@ -139,6 +142,7 @@ type UpdateProfissionalInput struct { TabelaFree *string `json:"tabela_free"` ExtraPorEquipamento *bool `json:"extra_por_equipamento"` Equipamentos *string `json:"equipamentos"` + Email *string `json:"email"` AvatarURL *string `json:"avatar_url"` } @@ -178,6 +182,7 @@ func (s *Service) Update(ctx context.Context, id string, input UpdateProfissiona TabelaFree: toPgText(input.TabelaFree), ExtraPorEquipamento: toPgBool(input.ExtraPorEquipamento), Equipamentos: toPgText(input.Equipamentos), + Email: toPgText(input.Email), AvatarUrl: toPgText(input.AvatarURL), } @@ -224,6 +229,8 @@ func toPgNumeric(f *float64) pgtype.Numeric { return pgtype.Numeric{Valid: false} } var n pgtype.Numeric - n.Scan(f) + if err := n.Scan(fmt.Sprintf("%f", *f)); err != nil { + return pgtype.Numeric{Valid: false} + } return n } diff --git a/frontend/pages/Team.tsx b/frontend/pages/Team.tsx index 7014c5d..1f72a69 100644 --- a/frontend/pages/Team.tsx +++ b/frontend/pages/Team.tsx @@ -17,264 +17,187 @@ import { Car, Building, CreditCard, - TrendingUp, + Trash2, + Edit2, AlertTriangle, + Check, + DollarSign, } from "lucide-react"; import { Button } from "../components/Button"; -import { getProfessionalRoles } from "../services/apiService"; - -type ProfessionalRole = "Fotógrafo" | "Cinegrafista" | "Recepcionista"; - -interface Professional { - id: string; - name: string; - role: ProfessionalRole; - address: { - street: string; - number: string; - complement?: string; - neighborhood: string; - city: string; - state: string; - }; - whatsapp: string; - cpfCnpj: string; - bankInfo: { - bank: string; - agency: string; - accountPix: string; - }; - hasCar: boolean; - hasStudio: boolean; - studioQuantity?: number; - cardType: string; - accountHolder: string; - observations?: string; - ratings: { - technicalQuality: number; - appearance: number; - education: number; - sympathy: number; - eventPerformance: number; - scheduleAvailability: number; - average: number; - }; - freeTable: string; - extraFee: string; - email: string; - specialties: string[]; - eventsCompleted: number; - status: "active" | "inactive" | "busy"; - avatar: string; - joinDate: string; -} - -const MOCK_PROFESSIONALS: Professional[] = [ - { - id: "1", - name: "Carlos Silva", - role: "Fotógrafo", - address: { - street: "Rua das Flores", - number: "123", - complement: "Apto 301", - neighborhood: "Centro", - city: "Curitiba", - state: "PR", - }, - whatsapp: "(41) 99999-1111", - cpfCnpj: "123.456.789-00", - bankInfo: { - bank: "Banco do Brasil", - agency: "1234-5", - accountPix: "carlos.silva@email.com", - }, - hasCar: true, - hasStudio: false, - cardType: "Débito/Crédito", - accountHolder: "Carlos Silva", - ratings: { - technicalQuality: 4.8, - appearance: 4.7, - education: 4.9, - sympathy: 4.8, - eventPerformance: 4.9, - scheduleAvailability: 4.6, - average: 4.8, - }, - freeTable: "R$ 800,00", - extraFee: "R$ 150,00 por equipamento extra", - email: "carlos.silva@photum.com", - specialties: ["Formaturas", "Eventos Corporativos"], - eventsCompleted: 45, - status: "active", - avatar: "https://i.pravatar.cc/150?img=12", - joinDate: "2023-01-15", - }, - { - id: "2", - name: "Ana Paula Mendes", - role: "Cinegrafista", - address: { - street: "Av. Sete de Setembro", - number: "456", - neighborhood: "Batel", - city: "Curitiba", - state: "PR", - }, - whatsapp: "(41) 99999-2222", - cpfCnpj: "987.654.321-00", - bankInfo: { - bank: "Caixa Econômica", - agency: "0987", - accountPix: "(41) 99999-2222", - }, - hasCar: true, - hasStudio: true, - studioQuantity: 1, - cardType: "Crédito", - accountHolder: "Ana Paula Mendes", - ratings: { - technicalQuality: 4.9, - appearance: 5.0, - education: 4.8, - sympathy: 4.9, - eventPerformance: 4.9, - scheduleAvailability: 4.7, - average: 4.9, - }, - freeTable: "R$ 1.200,00", - extraFee: "R$ 200,00 por hora extra", - email: "ana.mendes@photum.com", - specialties: ["Casamentos", "Formaturas"], - eventsCompleted: 62, - status: "busy", - avatar: "https://i.pravatar.cc/150?img=5", - joinDate: "2022-08-20", - }, - { - id: "3", - name: "Mariana Alves", - role: "Recepcionista", - address: { - street: "Rua Comendador Araújo", - number: "321", - complement: "Sala 12", - neighborhood: "Centro", - city: "Curitiba", - state: "PR", - }, - whatsapp: "(41) 99999-4444", - cpfCnpj: "321.654.987-00", - bankInfo: { - bank: "Bradesco", - agency: "4321", - accountPix: "mariana.alves@email.com", - }, - hasCar: false, - hasStudio: false, - cardType: "Débito", - accountHolder: "Mariana Alves Santos", - ratings: { - technicalQuality: 0, - appearance: 4.9, - education: 5.0, - sympathy: 5.0, - eventPerformance: 4.8, - scheduleAvailability: 4.9, - average: 4.9, - }, - freeTable: "R$ 400,00", - extraFee: "R$ 50,00 por hora extra", - email: "mariana.alves@photum.com", - specialties: ["Recepção", "Atendimento"], - eventsCompleted: 71, - status: "active", - avatar: "https://i.pravatar.cc/150?img=9", - joinDate: "2022-05-12", - }, -]; +import { + getFunctions, + createProfessional, + getProfessionals, + updateProfessional, + deleteProfessional, + getUploadURL, + uploadFileToSignedUrl, +} from "../services/apiService"; +import { useAuth } from "../contexts/AuthContext"; +import { Professional, CreateProfessionalDTO } from "../types"; export const TeamPage: React.FC = () => { + const { user, token: contextToken } = useAuth(); + const token = contextToken || ""; + + // Lists + const [professionals, setProfessionals] = useState([]); + const [roles, setRoles] = useState<{ id: string; nome: string }[]>([]); + + // Loading States + const [isLoading, setIsLoading] = useState(true); + const [isBackendDown, setIsBackendDown] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isLoadingCep, setIsLoadingCep] = useState(false); + + // Filters const [searchTerm, setSearchTerm] = useState(""); - const [roleFilter, setRoleFilter] = useState("all"); - const [statusFilter, setStatusFilter] = useState< - "all" | "active" | "busy" | "inactive" - >("all"); - const [selectedProfessional, setSelectedProfessional] = - useState(null); + const [roleFilter, setRoleFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + + // Selection & Modals + const [selectedProfessional, setSelectedProfessional] = useState(null); const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); - const [editingProfessional, setEditingProfessional] = useState(null); - const [professionalRoles, setProfessionalRoles] = useState([]); - const [isBackendDown, setIsBackendDown] = useState(false); - const [isLoadingRoles, setIsLoadingRoles] = useState(true); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [professionalToDelete, setProfessionalToDelete] = useState(null); + const [viewProfessional, setViewProfessional] = useState(null); - const [newProfessional, setNewProfessional] = useState>( - { - name: "", - role: "Fotógrafo", - address: { - street: "", - number: "", - complement: "", - neighborhood: "", - city: "", - state: "", - }, - whatsapp: "", - cpfCnpj: "", - bankInfo: { - bank: "", - agency: "", - accountPix: "", - }, - hasCar: false, - hasStudio: false, - studioQuantity: 0, - cardType: "", - accountHolder: "", - observations: "", - ratings: { - technicalQuality: 0, - appearance: 0, - education: 0, - sympathy: 0, - eventPerformance: 0, - scheduleAvailability: 0, - average: 0, - }, - freeTable: "", - extraFee: "", - email: "", - specialties: [], - avatar: "", - } - ); + // Form State + const initialFormState: CreateProfessionalDTO = { + nome: "", + funcao_profissional_id: "", + email: "", + whatsapp: "", + cpf_cnpj_titular: "", + endereco: "", + cidade: "", + uf: "", + banco: "", + agencia: "", + conta_pix: "", + tipo_cartao: "", + carro_disponivel: false, + tem_estudio: false, + qtd_estudio: 0, + observacao: "", + qual_tec: 0, + educacao_simpatia: 0, + desempenho_evento: 0, + disp_horario: 0, + media: 0, + tabela_free: "", + extra_por_equipamento: false, + equipamentos: "", + avatar_url: "", + }; + const [formData, setFormData] = useState(initialFormState); const [avatarFile, setAvatarFile] = useState(null); const [avatarPreview, setAvatarPreview] = useState(""); - // Buscar funções profissionais do backend + // Fetch Data useEffect(() => { - const fetchRoles = async () => { - setIsLoadingRoles(true); - const response = await getProfessionalRoles(); - - if (response.isBackendDown) { - setIsBackendDown(true); - setProfessionalRoles([]); - } else if (response.data) { + fetchData(); + }, [token]); + + const fetchData = async () => { + setIsLoading(true); + try { + const [rolesData, prosData] = await Promise.all([ + getFunctions(), + getProfessionals(token), + ]); + + if (rolesData.data) setRoles(rolesData.data); + if (prosData.data) { + setProfessionals(prosData.data); setIsBackendDown(false); - setProfessionalRoles(response.data); + } else if (prosData.error) { + console.error("Error fetching professionals:", prosData.error); + if (prosData.isBackendDown) setIsBackendDown(true); } - - setIsLoadingRoles(false); - }; + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setIsLoading(false); + } + }; - fetchRoles(); - }, []); + // Helpers + const GenericAvatar = "https://ui-avatars.com/api/?background=random"; + const ufs = [ + "AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO" + ]; + + const maskPhone = (value: string) => { + return value + .replace(/\D/g, "") + .replace(/^(\d{2})(\d)/g, "($1) $2") + .replace(/(\d)(\d{4})$/, "$1-$2") + .slice(0, 15); + }; + + const maskCpfCnpj = (value: string) => { + const clean = value.replace(/\D/g, ""); + if (clean.length <= 11) { + return clean + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d)/, "$1.$2") + .replace(/(\d{3})(\d{1,2})/, "$1-$2") + .replace(/(-\d{2})\d+?$/, "$1"); // Captures 11 digits + } else { + return clean + .replace(/^(\d{2})(\d)/, "$1.$2") + .replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3") + .replace(/\.(\d{3})(\d)/, ".$1/$2") + .replace(/(\d{4})(\d)/, "$1-$2") + .replace(/(-\d{2})\d+?$/, "$1") // Captures 14 digits + .slice(0, 18); + } + }; + + const calculateMedia = (ratings: { + qual_tec: number; + educacao_simpatia: number; + desempenho_evento: number; + disp_horario: number; + }) => { + const weightedScore = + ratings.qual_tec * 2 + + ratings.educacao_simpatia + + ratings.desempenho_evento + + ratings.disp_horario; + return weightedScore / 5; + }; + + const handleCepBlur = async () => { + const cep = formData.cep?.replace(/\D/g, "") || ""; + if (cep.length !== 8) return; + + setIsLoadingCep(true); + try { + const response = await fetch( + `https://cep.awesomeapi.com.br/json/${cep}` + ); + if (!response.ok) throw new Error("CEP não encontrado"); + const data = await response.json(); + + setFormData((prev) => ({ + ...prev, + endereco: `${data.address || ""} ${data.district ? `- ${data.district}` : ""}`.trim() || prev.endereco, + cidade: data.city || prev.cidade, + uf: data.state || prev.uf, + })); + } catch (error) { + console.error("Erro ao buscar CEP:", error); + } finally { + setIsLoadingCep(false); + } + }; + + // Handlers const handleAvatarChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -290,119 +213,161 @@ export const TeamPage: React.FC = () => { const removeAvatar = () => { setAvatarFile(null); setAvatarPreview(""); + setFormData((prev) => ({ ...prev, avatar_url: "" })); }; - const handleEditProfessional = (professional: Professional) => { - setEditingProfessional(professional); - setNewProfessional(professional); - setAvatarPreview(professional.avatar); + const resetForm = () => { + setFormData(initialFormState); + setAvatarFile(null); + setAvatarPreview(""); setSelectedProfessional(null); + }; + + const handleEditClick = (professional: Professional) => { + setFormData({ + nome: professional.nome, + funcao_profissional_id: professional.funcao_profissional_id, + email: professional.email || "", + whatsapp: professional.whatsapp || "", + cpf_cnpj_titular: professional.cpf_cnpj_titular || "", + endereco: professional.endereco || "", + cidade: professional.cidade || "", + uf: professional.uf || "", + cep: professional.cep || "", + banco: professional.banco || "", + agencia: professional.agencia || "", + conta_pix: professional.conta_pix || "", + tipo_cartao: professional.tipo_cartao || "", + carro_disponivel: professional.carro_disponivel || false, + tem_estudio: professional.tem_estudio || false, + qtd_estudio: professional.qtd_estudio || 0, + observacao: professional.observacao || "", + qual_tec: professional.qual_tec || 0, + educacao_simpatia: professional.educacao_simpatia || 0, + desempenho_evento: professional.desempenho_evento || 0, + disp_horario: professional.disp_horario || 0, + tabela_free: professional.tabela_free || "", + extra_por_equipamento: professional.extra_por_equipamento || false, + equipamentos: professional.equipamentos || "", + avatar_url: professional.avatar_url || "", + media: professional.media || 0, + }); + setAvatarPreview(professional.avatar_url || (professional.avatar ?? GenericAvatar)); + setAvatarFile(null); + setSelectedProfessional(professional); // Storing the professional being edited here setShowEditModal(true); }; - const handleUpdateProfessional = () => { - // Aqui você implementaria a lógica de atualização - // Por exemplo, chamada à API para atualizar os dados - console.log('Atualizando profissional:', newProfessional); - - // Simulando atualização - setShowEditModal(false); - setEditingProfessional(null); - setNewProfessional({ - name: "", - role: "Fotógrafo", - address: { - street: "", - number: "", - complement: "", - neighborhood: "", - city: "", - state: "", - }, - whatsapp: "", - cpfCnpj: "", - bankInfo: { - bank: "", - agency: "", - accountPix: "", - }, - hasCar: false, - hasStudio: false, - studioQuantity: 0, - cardType: "", - accountHolder: "", - observations: "", - ratings: { - technicalQuality: 0, - appearance: 0, - education: 0, - sympathy: 0, - eventPerformance: 0, - scheduleAvailability: 0, - average: 0, - }, - freeTable: "", - extraFee: "", - email: "", - specialties: [], - avatar: "", + const handleViewClick = (professional: Professional) => { + setViewProfessional(professional); + }; + + // Update Media when ratings change + useEffect(() => { + const newMedia = calculateMedia({ + qual_tec: formData.qual_tec || 0, + educacao_simpatia: formData.educacao_simpatia || 0, + desempenho_evento: formData.desempenho_evento || 0, + disp_horario: formData.disp_horario || 0, }); - removeAvatar(); - }; + // Only update if it's different to avoid loops/excessive renders, though optional in this simple case + setFormData((prev) => { + if (prev.media === newMedia) return prev; + return { ...prev, media: newMedia }; + }); + }, [ + formData.qual_tec, + formData.educacao_simpatia, + formData.desempenho_evento, + formData.disp_horario, + ]); - const getStatusColor = (status: Professional["status"]) => { - switch (status) { - case "active": - return "bg-green-100 text-green-800"; - case "busy": - return "bg-yellow-100 text-yellow-800"; - case "inactive": - return "bg-gray-100 text-gray-800"; + const handleSubmit = async (e: React.FormEvent, isEdit: boolean) => { + e.preventDefault(); + setIsSubmitting(true); + + try { + let finalAvatarUrl = formData.avatar_url; + + // Handle Avatar Upload if new file selected + if (avatarFile) { + const uploadRes = await getUploadURL(avatarFile.name, avatarFile.type); + if (uploadRes.data) { + await uploadFileToSignedUrl(uploadRes.data.upload_url, avatarFile); + finalAvatarUrl = uploadRes.data.public_url; + } + } + + const payload = { ...formData, avatar_url: finalAvatarUrl }; + + if (isEdit && selectedProfessional) { + await updateProfessional(selectedProfessional.id, payload, token); + alert("Profissional atualizado com sucesso!"); + } else { + await createProfessional(payload, token); + alert("Profissional criado com sucesso!"); + } + + setShowAddModal(false); + setShowEditModal(false); + fetchData(); + // Reset form + // Reset form + resetForm(); + } catch (error) { + console.error("Error submitting form:", error); + alert("Erro ao salvar profissional. Verifique o console."); + } finally { + setIsSubmitting(false); } }; - const getStatusLabel = (status: Professional["status"]) => { - switch (status) { - case "active": - return "Disponível"; - case "busy": - return "Em Evento"; - case "inactive": - return "Inativo"; + const handleDelete = async () => { + if (!professionalToDelete) return; + try { + await deleteProfessional(professionalToDelete.id, token); + setShowDeleteModal(false); + setProfessionalToDelete(null); + fetchData(); + } catch (error) { + console.error("Error deleting professional:", error); + alert("Erro ao excluir profissional."); } }; - const getRoleIcon = (role: ProfessionalRole) => { - switch (role) { - case "Fotógrafo": - return Camera; - case "Cinegrafista": - return Video; - case "Recepcionista": - return UserCheck; - } + // Helper renderers + const getRoleName = (id: string) => { + return roles.find((r) => r.id === id)?.nome || "Desconhecido"; }; - const filteredProfessionals = MOCK_PROFESSIONALS.filter((professional) => { + const getRoleIcon = (roleName: string) => { + const lower = roleName.toLowerCase(); + if (lower.includes("foto")) return Camera; + if (lower.includes("video") || lower.includes("cine")) return Video; + return UserCheck; + }; + + // Filter Logic + const filteredProfessionals = professionals.filter((p) => { const matchesSearch = - professional.name.toLowerCase().includes(searchTerm.toLowerCase()) || - professional.email.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesRole = - roleFilter === "all" || professional.role === roleFilter; - const matchesStatus = - statusFilter === "all" || professional.status === statusFilter; - return matchesSearch && matchesRole && matchesStatus; + p.nome.toLowerCase().includes(searchTerm.toLowerCase()) || + (p.email && p.email.toLowerCase().includes(searchTerm.toLowerCase())); + + // Adjusted role logic since we have ID in database but names in roles array + const roleName = getRoleName(p.funcao_profissional_id); + const matchesRole = roleFilter === "all" || roleName === roleFilter; + + // Hide users with unknown roles + if (roleName === "Desconhecido") return false; + + return matchesSearch && matchesRole; }); const stats = { - photographers: MOCK_PROFESSIONALS.filter((p) => p.role === "Fotógrafo") - .length, - cinematographers: MOCK_PROFESSIONALS.filter( - (p) => p.role === "Cinegrafista" - ).length, - receptionists: MOCK_PROFESSIONALS.filter((p) => p.role === "Recepcionista") - .length, - total: MOCK_PROFESSIONALS.length, + total: professionals.length, + photographers: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("fot") || getRoleName(p.funcao_profissional_id).toLowerCase().includes("foto")).length, + cine: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("cine") || getRoleName(p.funcao_profissional_id).toLowerCase().includes("video")).length, + recep: professionals.filter(p => getRoleName(p.funcao_profissional_id).toLowerCase().includes("recep")).length, }; return ( @@ -419,56 +384,37 @@ export const TeamPage: React.FC = () => { {/* Stats */} -
-
-
-
-

- Total de Fotógrafos -

-

- {stats.photographers} -

+
+ {roles.map(role => { + const count = professionals.filter(p => p.funcao_profissional_id === role.id).length; + const RoleIcon = getRoleIcon(role.nome); + + // Optional: Customize colors based on role or just cycle/default + // For simplicity using existing logic or default + let iconColorClass = "text-brand-black"; + if (role.nome.toLowerCase().includes("foto")) iconColorClass = "text-brand-gold"; + else if (role.nome.toLowerCase().includes("video") || role.nome.toLowerCase().includes("cine")) iconColorClass = "text-blue-600"; + else if (role.nome.toLowerCase().includes("recep")) iconColorClass = "text-purple-600"; + + return ( +
+
+
+

Total de {role.nome}s

+

{count}

+
+ +
- -
-
-
-
-
-

- Total de Cinegrafistas -

-

- {stats.cinematographers} -

-
-
-
-
-
-
-

- Total de Recepcionistas -

-

- {stats.receptionists} -

-
- -
-
+ ); + })}
{/* Filters and Search */} -
-
-
- +
+
+
+ { className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" />
-
- - - - -
- {/* Status Filters */} -
- - - - -
- -
- {/* Professionals Table */} -
- {filteredProfessionals.length === 0 ? ( -
- -

Nenhum profissional encontrado

-
- ) : ( + {/* List */} + {isLoading ? ( +
Carregando...
+ ) : ( +
- - - + + + + - - {filteredProfessionals.map((professional) => { - const RoleIcon = getRoleIcon(professional.role); + + {filteredProfessionals.map((p) => { + const roleName = getRoleName(p.funcao_profissional_id); + const RoleIcon = getRoleIcon(roleName); return ( - setSelectedProfessional(professional)} - > + handleViewClick(p)}> + ); @@ -652,1014 +502,421 @@ export const TeamPage: React.FC = () => {
- Nome - - Função Profissional - - Disponibilidade - ProfissionalFunçãoContatoAções
-
-
-
- {professional.name} -
-
- - - {professional.ratings.average.toFixed(1)} - - - ({professional.eventsCompleted} eventos) - +
+ {p.nome} +
+
{p.nome}
+
+ + {p.media ? p.media.toFixed(1) : "N/A"}
-
- - - {professional.role} - +
+ + {roleName}
- - {getStatusLabel(professional.status)} - +
{p.whatsapp}
+
{p.email}
+
+ +
- )} -
+
+ )}
- {/* Edit Professional Modal */} - {showEditModal && ( -
setShowEditModal(false)} - > -
e.stopPropagation()} - > -
-
-

- Editar Profissional -

-

- Atualize as informações do profissional -

-
- + {/* Add/Edit Modal */} + {(showAddModal || showEditModal) && ( +
+
+
+

{showEditModal ? "Editar Profissional" : "Novo Profissional"}

+
-
{ - e.preventDefault(); - handleUpdateProfessional(); - alert("Profissional atualizado com sucesso!"); - }} - > - {/* Reutilizar o mesmo conteúdo do formulário de adicionar */} - {/* Avatar Upload */} -
- -
- {avatarPreview ? ( -
- Preview - -
- ) : ( -
- -
+ handleSubmit(e, showEditModal)} className="space-y-6"> + + {/* Photo */} +
+
+
+ {avatarPreview ? ( + Preview + ) : ( + + )} +
+ + {avatarPreview && ( + )} -
+
+ + {/* Basic Info */} +
+
+ + setFormData({ ...formData, nome: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + +
+
+ + setFormData({ ...formData, email: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, whatsapp: maskPhone(e.target.value) })} maxLength={15} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, cpf_cnpj_titular: maskCpfCnpj(e.target.value) })} maxLength={18} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ +

Endereço

+
+
+ +
{ + const val = e.target.value.replace(/\D/g, "").replace(/^(\d{5})(\d)/, "$1-$2"); + setFormData({ ...formData, cep: val }); + }} + onBlur={handleCepBlur} + className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" + placeholder="00000-000" /> + {isLoadingCep && Buscando...} +
+
+
+ + setFormData({ ...formData, endereco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, cidade: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + +
+
+ + {/* Banking */} +

Dados Bancários

+
+
+ + setFormData({ ...formData, banco: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, agencia: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, conta_pix: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" /> +
+
+ + setFormData({ ...formData, tipo_cartao: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-brand-gold focus:ring focus:ring-brand-gold focus:ring-opacity-50 p-2 border" placeholder="SD, XQD..." /> +
+
+ + {/* Resources */} +

Recursos

+
+ + + {formData.tem_estudio && ( +
+ + setFormData({ ...formData, qtd_estudio: Math.max(0, parseInt(e.target.value)) })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+ )} +
+ + {/* Ratings */} +

Avaliações e Valores

+
+
+ + setFormData({ ...formData, qual_tec: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+
+ + setFormData({ ...formData, educacao_simpatia: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+
+ + setFormData({ ...formData, desempenho_evento: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+
+ + setFormData({ ...formData, disp_horario: parseInt(e.target.value) || 0 })} className="block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+
+ Média + {formData.media ? (typeof formData.media === 'number' ? formData.media.toFixed(1) : parseFloat(formData.media).toFixed(1)) : "0.0"} +
+
+ +
+
+ + setFormData({ ...formData, tabela_free: e.target.value })} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm p-2 border" /> +
+
+ + +
+ +
+ +
)} - {/* Add Professional Modal - com todos os campos da planilha */} - {showAddModal && ( -
setShowAddModal(false)} - > -
e.stopPropagation()} - > -
-

- Adicionar Novo Profissional -

- + {/* Delete Confirmation Modal */} + {showDeleteModal && ( +
+
+ +

Confirmar Exclusão

+

Tem certeza que deseja excluir {professionalToDelete?.nome}? Esta ação não pode ser desfeita.

+
+ +
- -
{ - e.preventDefault(); - alert("Profissional adicionado com sucesso!"); - setShowAddModal(false); - }} - > - {/* Dados Básicos */} -
-

- Dados Básicos -

- -
-
- - - setNewProfessional({ - ...newProfessional, - name: e.target.value, - }) - } - placeholder="Nome completo" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - {isBackendDown && ( -
- - Backend não está rodando. Não é possível carregar as funções profissionais. -
- )} - {isLoadingRoles && !isBackendDown && ( -
- Carregando funções profissionais... -
- )} -
-
- -
-
- - - setNewProfessional({ - ...newProfessional, - email: e.target.value, - }) - } - placeholder="email@exemplo.com" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - whatsapp: e.target.value, - }) - } - placeholder="(00) 00000-0000" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- -
- - - setNewProfessional({ - ...newProfessional, - cpfCnpj: e.target.value, - }) - } - placeholder="000.000.000-00 ou 00.000.000/0000-00" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- - {/* Endereço */} -
-

- Endereço -

- -
-
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - street: e.target.value, - }, - }) - } - placeholder="Nome da rua" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - number: e.target.value, - }, - }) - } - placeholder="123" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- -
-
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - complement: e.target.value, - }, - }) - } - placeholder="Apto, Sala, etc" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - neighborhood: e.target.value, - }, - }) - } - placeholder="Nome do bairro" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- -
-
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - city: e.target.value, - }, - }) - } - placeholder="Nome da cidade" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - address: { - ...newProfessional.address!, - state: e.target.value.toUpperCase(), - }, - }) - } - placeholder="SP" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
-
- - {/* Dados Bancários */} -
-

- Dados Bancários -

- -
-
- - - setNewProfessional({ - ...newProfessional, - bankInfo: { - ...newProfessional.bankInfo!, - bank: e.target.value, - }, - }) - } - placeholder="Nome do banco" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - bankInfo: { - ...newProfessional.bankInfo!, - agency: e.target.value, - }, - }) - } - placeholder="0000-0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - bankInfo: { - ...newProfessional.bankInfo!, - accountPix: e.target.value, - }, - }) - } - placeholder="Conta ou chave Pix" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- -
-
- - - setNewProfessional({ - ...newProfessional, - cardType: e.target.value, - }) - } - placeholder="Débito, Crédito, Pix, etc" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - accountHolder: e.target.value, - }) - } - placeholder="Nome do titular" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
-
- - {/* Recursos e Equipamentos */} -
-

- Recursos e Equipamentos -

- -
-
- -
- -
- -
- - {newProfessional.hasStudio && ( -
- - - setNewProfessional({ - ...newProfessional, - studioQuantity: parseInt(e.target.value), - }) - } - placeholder="0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- )} -
- -
- - - setNewProfessional({ - ...newProfessional, - cardType: e.target.value, - }) - } - placeholder="Ex: SD, CF, XQD, etc." - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
- - {/* Avaliações */} -
-

- Avaliações (0 a 5) -

- -
-
- - - setNewProfessional({ - ...newProfessional, - ratings: { - ...newProfessional.ratings!, - technicalQuality: parseFloat(e.target.value), - }, - }) - } - placeholder="0.0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - ratings: { - ...newProfessional.ratings!, - sympathy: parseFloat(e.target.value), - }, - }) - } - placeholder="0.0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - ratings: { - ...newProfessional.ratings!, - eventPerformance: parseFloat(e.target.value), - }, - }) - } - placeholder="0.0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - ratings: { - ...newProfessional.ratings!, - scheduleAvailability: parseFloat(e.target.value), - }, - }) - } - placeholder="0.0" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
-
- - {/* Valores */} -
-

- Valores -

- -
-
- - - setNewProfessional({ - ...newProfessional, - freeTable: e.target.value, - }) - } - placeholder="R$ 800,00" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
- -
- - - setNewProfessional({ - ...newProfessional, - extraFee: e.target.value, - }) - } - placeholder="R$ 150,00" - className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold" - /> -
-
-
- - {/* Observações */} -
- -