feat(ux): melhorias de navegação, formulário e fluxo de eventos
Backend: - Adiciona campo `contacts` (JSONB) na tabela `agendas` e atualiza lógica de criação. Frontend: - Adiciona campos dinâmicos de contato no formulário de Novo Evento. - Otimiza busca do Mapbox priorizando a região selecionada (SP/MG). - Implementa "Deep Linking" no Dashboard (abrir detalhes do evento direto via URL). - Corrige "flicker" (piscada da lista) ao carregar detalhes via link permitindo carregamento suave. - Adiciona botão "Aprovar" e fluxo de aprovação na visualização de detalhes. - Corrige fluxo de edição (salvar retorna para detalhes sem recarregar a página). - Corrige navegação dos botões "Voltar" em Detalhes e Logística para retornarem corretamente à lista/painel. - Melhora layout do cabeçalho de detalhes (remove ID vazio e unifica títulos duplicados). - Ajusta clique no Logo para forçar reset da navegação para o Painel.
This commit is contained in:
parent
e86bd0a570
commit
21987d221e
13 changed files with 208 additions and 61 deletions
|
|
@ -32,30 +32,37 @@ func NewService(db *generated.Queries, notif *notification.Service, cfg *config.
|
|||
}
|
||||
}
|
||||
|
||||
type ContactInfo struct {
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
type CreateAgendaRequest struct {
|
||||
FotID uuid.UUID `json:"fot_id"`
|
||||
DataEvento time.Time `json:"data_evento"`
|
||||
TipoEventoID uuid.UUID `json:"tipo_evento_id"`
|
||||
ObservacoesEvento string `json:"observacoes_evento"`
|
||||
LocalEvento string `json:"local_evento"`
|
||||
Endereco string `json:"endereco"`
|
||||
Horario string `json:"horario"`
|
||||
QtdFormandos int32 `json:"qtd_formandos"`
|
||||
QtdFotografos int32 `json:"qtd_fotografos"`
|
||||
QtdRecepcionistas int32 `json:"qtd_recepcionistas"`
|
||||
QtdCinegrafistas int32 `json:"qtd_cinegrafistas"`
|
||||
QtdEstudios int32 `json:"qtd_estudios"`
|
||||
QtdPontoFoto int32 `json:"qtd_ponto_foto"`
|
||||
QtdPontoID int32 `json:"qtd_ponto_id"`
|
||||
QtdPontoDecorado int32 `json:"qtd_ponto_decorado"`
|
||||
QtdPontosLed int32 `json:"qtd_pontos_led"`
|
||||
QtdPlataforma360 int32 `json:"qtd_plataforma_360"`
|
||||
StatusProfissionais string `json:"status_profissionais"`
|
||||
FotoFaltante int32 `json:"foto_faltante"`
|
||||
RecepFaltante int32 `json:"recep_faltante"`
|
||||
CineFaltante int32 `json:"cine_faltante"`
|
||||
LogisticaObservacoes string `json:"logistica_observacoes"`
|
||||
PreVenda bool `json:"pre_venda"`
|
||||
FotID uuid.UUID `json:"fot_id"`
|
||||
DataEvento time.Time `json:"data_evento"`
|
||||
TipoEventoID uuid.UUID `json:"tipo_evento_id"`
|
||||
ObservacoesEvento string `json:"observacoes_evento"`
|
||||
LocalEvento string `json:"local_evento"`
|
||||
Endereco string `json:"endereco"`
|
||||
Horario string `json:"horario"`
|
||||
QtdFormandos int32 `json:"qtd_formandos"`
|
||||
QtdFotografos int32 `json:"qtd_fotografos"`
|
||||
QtdRecepcionistas int32 `json:"qtd_recepcionistas"`
|
||||
QtdCinegrafistas int32 `json:"qtd_cinegrafistas"`
|
||||
QtdEstudios int32 `json:"qtd_estudios"`
|
||||
QtdPontoFoto int32 `json:"qtd_ponto_foto"`
|
||||
QtdPontoID int32 `json:"qtd_ponto_id"`
|
||||
QtdPontoDecorado int32 `json:"qtd_ponto_decorado"`
|
||||
QtdPontosLed int32 `json:"qtd_pontos_led"`
|
||||
QtdPlataforma360 int32 `json:"qtd_plataforma_360"`
|
||||
StatusProfissionais string `json:"status_profissionais"`
|
||||
FotoFaltante int32 `json:"foto_faltante"`
|
||||
RecepFaltante int32 `json:"recep_faltante"`
|
||||
CineFaltante int32 `json:"cine_faltante"`
|
||||
LogisticaObservacoes string `json:"logistica_observacoes"`
|
||||
PreVenda bool `json:"pre_venda"`
|
||||
Contatos []ContactInfo `json:"contatos"`
|
||||
}
|
||||
|
||||
type Assignment struct {
|
||||
|
|
@ -67,7 +74,8 @@ type Assignment struct {
|
|||
|
||||
type AgendaResponse struct {
|
||||
generated.ListAgendasRow
|
||||
ParsedAssignments []Assignment `json:"assignments"`
|
||||
ParsedAssignments []Assignment `json:"assignments"`
|
||||
ParsedContacts []ContactInfo `json:"contacts"`
|
||||
}
|
||||
|
||||
func (s *Service) CalculateStatus(fotoFaltante, recepFaltante, cineFaltante int32) string {
|
||||
|
|
@ -88,6 +96,8 @@ func (s *Service) CalculateStatus(fotoFaltante, recepFaltante, cineFaltante int3
|
|||
func (s *Service) Create(ctx context.Context, userID uuid.UUID, req CreateAgendaRequest, regiao string) (generated.Agenda, error) {
|
||||
status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante)
|
||||
|
||||
contatosBytes, _ := json.Marshal(req.Contatos)
|
||||
|
||||
params := generated.CreateAgendaParams{
|
||||
FotID: pgtype.UUID{Bytes: req.FotID, Valid: true},
|
||||
DataEvento: pgtype.Date{Time: req.DataEvento, Valid: true},
|
||||
|
|
@ -114,6 +124,7 @@ func (s *Service) Create(ctx context.Context, userID uuid.UUID, req CreateAgenda
|
|||
PreVenda: pgtype.Bool{Bool: req.PreVenda, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
Regiao: pgtype.Text{String: regiao, Valid: true},
|
||||
Contatos: contatosBytes,
|
||||
}
|
||||
|
||||
return s.queries.CreateAgenda(ctx, params)
|
||||
|
|
@ -183,6 +194,7 @@ func (s *Service) List(ctx context.Context, userID uuid.UUID, role string, regia
|
|||
ObservacoesFot: r.ObservacoesFot,
|
||||
TipoEventoNome: r.TipoEventoNome,
|
||||
AssignedProfessionals: r.AssignedProfessionals,
|
||||
Contatos: r.Contatos,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
@ -207,9 +219,16 @@ func (s *Service) List(ctx context.Context, userID uuid.UUID, role string, regia
|
|||
json.Unmarshal(bytes, &assignments)
|
||||
}
|
||||
}
|
||||
|
||||
var contacts []ContactInfo
|
||||
if len(row.Contatos) > 0 {
|
||||
json.Unmarshal(row.Contatos, &contacts)
|
||||
}
|
||||
|
||||
response = append(response, AgendaResponse{
|
||||
ListAgendasRow: row,
|
||||
ParsedAssignments: assignments,
|
||||
ParsedContacts: contacts,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +252,8 @@ func (s *Service) Update(ctx context.Context, id uuid.UUID, req CreateAgendaRequ
|
|||
|
||||
status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante)
|
||||
|
||||
contatosBytes, _ := json.Marshal(req.Contatos)
|
||||
|
||||
params := generated.UpdateAgendaParams{
|
||||
ID: pgtype.UUID{Bytes: id, Valid: true},
|
||||
FotID: pgtype.UUID{Bytes: req.FotID, Valid: true},
|
||||
|
|
@ -259,6 +280,7 @@ func (s *Service) Update(ctx context.Context, id uuid.UUID, req CreateAgendaRequ
|
|||
LogisticaObservacoes: pgtype.Text{String: req.LogisticaObservacoes, Valid: req.LogisticaObservacoes != ""},
|
||||
PreVenda: pgtype.Bool{Bool: req.PreVenda, Valid: true},
|
||||
Regiao: pgtype.Text{String: regiao, Valid: true},
|
||||
Contatos: contatosBytes,
|
||||
}
|
||||
return s.queries.UpdateAgenda(ctx, params)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,10 +97,11 @@ INSERT INTO agenda (
|
|||
logistica_observacoes,
|
||||
pre_venda,
|
||||
user_id,
|
||||
regiao
|
||||
regiao,
|
||||
contatos
|
||||
) 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, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $26, $25
|
||||
) RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao, contatos
|
||||
`
|
||||
|
||||
type CreateAgendaParams struct {
|
||||
|
|
@ -128,6 +129,7 @@ type CreateAgendaParams struct {
|
|||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +159,7 @@ func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Age
|
|||
arg.LogisticaObservacoes,
|
||||
arg.PreVenda,
|
||||
arg.UserID,
|
||||
arg.Contatos,
|
||||
arg.Regiao,
|
||||
)
|
||||
var i Agenda
|
||||
|
|
@ -191,6 +194,7 @@ func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Age
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -211,7 +215,7 @@ func (q *Queries) DeleteAgenda(ctx context.Context, arg DeleteAgendaParams) erro
|
|||
}
|
||||
|
||||
const getAgenda = `-- name: GetAgenda :one
|
||||
SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao FROM agenda
|
||||
SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao, contatos FROM agenda
|
||||
WHERE id = $1 AND regiao = $2 LIMIT 1
|
||||
`
|
||||
|
||||
|
|
@ -254,12 +258,13 @@ func (q *Queries) GetAgenda(ctx context.Context, arg GetAgendaParams) (Agenda, e
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAgendaByFotDataTipo = `-- name: GetAgendaByFotDataTipo :one
|
||||
SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao FROM agenda
|
||||
SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao, contatos FROM agenda
|
||||
WHERE fot_id = $1 AND data_evento = $2 AND tipo_evento_id = $3 AND regiao = $4
|
||||
LIMIT 1
|
||||
`
|
||||
|
|
@ -310,6 +315,7 @@ func (q *Queries) GetAgendaByFotDataTipo(ctx context.Context, arg GetAgendaByFot
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -413,7 +419,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
|
|||
|
||||
const listAgendas = `-- name: ListAgendas :many
|
||||
SELECT
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao,
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
|
|
@ -474,6 +480,7 @@ type ListAgendasRow struct {
|
|||
Status pgtype.Text `json:"status"`
|
||||
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
FotNumero string `json:"fot_numero"`
|
||||
Instituicao pgtype.Text `json:"instituicao"`
|
||||
CursoNome string `json:"curso_nome"`
|
||||
|
|
@ -525,6 +532,7 @@ func (q *Queries) ListAgendas(ctx context.Context, regiao pgtype.Text) ([]ListAg
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
&i.FotNumero,
|
||||
&i.Instituicao,
|
||||
&i.CursoNome,
|
||||
|
|
@ -547,7 +555,7 @@ func (q *Queries) ListAgendas(ctx context.Context, regiao pgtype.Text) ([]ListAg
|
|||
|
||||
const listAgendasByCompany = `-- name: ListAgendasByCompany :many
|
||||
SELECT
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao,
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
|
|
@ -613,6 +621,7 @@ type ListAgendasByCompanyRow struct {
|
|||
Status pgtype.Text `json:"status"`
|
||||
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
FotNumero string `json:"fot_numero"`
|
||||
Instituicao pgtype.Text `json:"instituicao"`
|
||||
CursoNome string `json:"curso_nome"`
|
||||
|
|
@ -664,6 +673,7 @@ func (q *Queries) ListAgendasByCompany(ctx context.Context, arg ListAgendasByCom
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
&i.FotNumero,
|
||||
&i.Instituicao,
|
||||
&i.CursoNome,
|
||||
|
|
@ -686,7 +696,7 @@ func (q *Queries) ListAgendasByCompany(ctx context.Context, arg ListAgendasByCom
|
|||
|
||||
const listAgendasByFot = `-- name: ListAgendasByFot :many
|
||||
SELECT
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao,
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
||||
te.nome as tipo_evento_nome
|
||||
FROM agenda a
|
||||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||
|
|
@ -730,6 +740,7 @@ type ListAgendasByFotRow struct {
|
|||
Status pgtype.Text `json:"status"`
|
||||
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
TipoEventoNome string `json:"tipo_evento_nome"`
|
||||
}
|
||||
|
||||
|
|
@ -773,6 +784,7 @@ func (q *Queries) ListAgendasByFot(ctx context.Context, arg ListAgendasByFotPara
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
&i.TipoEventoNome,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -787,7 +799,7 @@ func (q *Queries) ListAgendasByFot(ctx context.Context, arg ListAgendasByFotPara
|
|||
|
||||
const listAgendasByUser = `-- name: ListAgendasByUser :many
|
||||
SELECT
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao,
|
||||
a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, a.regiao, a.contatos,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
|
|
@ -853,6 +865,7 @@ type ListAgendasByUserRow struct {
|
|||
Status pgtype.Text `json:"status"`
|
||||
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
FotNumero string `json:"fot_numero"`
|
||||
Instituicao pgtype.Text `json:"instituicao"`
|
||||
CursoNome string `json:"curso_nome"`
|
||||
|
|
@ -904,6 +917,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, arg ListAgendasByUserPa
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
&i.FotNumero,
|
||||
&i.Instituicao,
|
||||
&i.CursoNome,
|
||||
|
|
@ -1083,9 +1097,10 @@ SET
|
|||
cine_faltante = $22,
|
||||
logistica_observacoes = $23,
|
||||
pre_venda = $24,
|
||||
contatos = $25,
|
||||
atualizado_em = NOW()
|
||||
WHERE id = $1 AND regiao = $25
|
||||
RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao
|
||||
WHERE id = $1 AND regiao = $26
|
||||
RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao, contatos
|
||||
`
|
||||
|
||||
type UpdateAgendaParams struct {
|
||||
|
|
@ -1113,6 +1128,7 @@ type UpdateAgendaParams struct {
|
|||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
}
|
||||
|
||||
|
|
@ -1142,6 +1158,7 @@ func (q *Queries) UpdateAgenda(ctx context.Context, arg UpdateAgendaParams) (Age
|
|||
arg.CineFaltante,
|
||||
arg.LogisticaObservacoes,
|
||||
arg.PreVenda,
|
||||
arg.Contatos,
|
||||
arg.Regiao,
|
||||
)
|
||||
var i Agenda
|
||||
|
|
@ -1176,6 +1193,7 @@ func (q *Queries) UpdateAgenda(ctx context.Context, arg UpdateAgendaParams) (Age
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1184,7 +1202,7 @@ const updateAgendaStatus = `-- name: UpdateAgendaStatus :one
|
|||
UPDATE agenda
|
||||
SET status = $2, atualizado_em = NOW()
|
||||
WHERE id = $1 AND regiao = $3
|
||||
RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao
|
||||
RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em, regiao, contatos
|
||||
`
|
||||
|
||||
type UpdateAgendaStatusParams struct {
|
||||
|
|
@ -1227,6 +1245,7 @@ func (q *Queries) UpdateAgendaStatus(ctx context.Context, arg UpdateAgendaStatus
|
|||
&i.Status,
|
||||
&i.LogisticaNotificacaoEnviadaEm,
|
||||
&i.Regiao,
|
||||
&i.Contatos,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type Agenda struct {
|
|||
Status pgtype.Text `json:"status"`
|
||||
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||
Regiao pgtype.Text `json:"regiao"`
|
||||
Contatos []byte `json:"contatos"`
|
||||
}
|
||||
|
||||
type AgendaEscala struct {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE agenda ADD COLUMN IF NOT EXISTS contatos JSONB DEFAULT '[]'::jsonb;
|
||||
|
|
@ -24,9 +24,10 @@ INSERT INTO agenda (
|
|||
logistica_observacoes,
|
||||
pre_venda,
|
||||
user_id,
|
||||
regiao
|
||||
regiao,
|
||||
contatos
|
||||
) 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, @regiao
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, @regiao, $25
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetAgenda :one
|
||||
|
|
@ -121,6 +122,7 @@ SET
|
|||
cine_faltante = $22,
|
||||
logistica_observacoes = $23,
|
||||
pre_venda = $24,
|
||||
contatos = $25,
|
||||
atualizado_em = NOW()
|
||||
WHERE id = $1 AND regiao = @regiao
|
||||
RETURNING *;
|
||||
|
|
|
|||
|
|
@ -231,7 +231,8 @@ CREATE TABLE IF NOT EXISTS agenda (
|
|||
atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status VARCHAR(50) DEFAULT 'Pendente', -- Pendente, Aprovado, Arquivado
|
||||
logistica_notificacao_enviada_em TIMESTAMP,
|
||||
regiao CHAR(2) DEFAULT 'SP'
|
||||
regiao CHAR(2) DEFAULT 'SP',
|
||||
contatos JSONB DEFAULT '[]'::jsonb
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agenda_profissionais (
|
||||
|
|
|
|||
|
|
@ -398,8 +398,8 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// Validation
|
||||
if (!formData.name) return alert("Preencha o tipo de evento");
|
||||
// Validation (Observations are optional)
|
||||
// if (!formData.name) return alert("Preencha o tipo de evento");
|
||||
if (!formData.date) return alert("Preencha a data");
|
||||
if (!formData.attendees || parseInt(formData.attendees) <= 0) return alert("Preencha o número de formandos");
|
||||
|
||||
|
|
@ -455,7 +455,8 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
status_profissionais: "PENDING",
|
||||
cine_faltante: 0,
|
||||
logistica_observacoes: "",
|
||||
pre_venda: true
|
||||
pre_venda: true,
|
||||
contatos: formData.contacts
|
||||
};
|
||||
|
||||
// Submit to parent handler
|
||||
|
|
@ -1124,7 +1125,7 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-32 text-sm"
|
||||
placeholder={
|
||||
isClientRequest
|
||||
? "Qual o estilo do casamento? Quais fotos são indispensáveis? Fale um pouco sobre vocês..."
|
||||
? "Fale sobre..."
|
||||
: "Instruções técnicas..."
|
||||
}
|
||||
value={formData.briefing}
|
||||
|
|
@ -1169,6 +1170,16 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
setFormData({ ...formData, contacts: newContacts });
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label={idx === 0 ? "Telefone" : ""}
|
||||
placeholder="(11) 9..."
|
||||
value={contact.phone}
|
||||
onChange={(e) => {
|
||||
const newContacts = [...formData.contacts];
|
||||
newContacts[idx].phone = e.target.value;
|
||||
setFormData({ ...formData, contacts: newContacts });
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeContact(idx)}
|
||||
className={`mt-1 p-2 text-gray-400 hover:text-red-500 ${idx === 0 ? "mt-7" : ""
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Plus, Trash, User, Truck, Car, Send } from "lucide-react";
|
||||
import { Plus, Trash, User, Truck, Car, Send, ArrowLeft } from "lucide-react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { listCarros, createCarro, deleteCarro, addPassenger, removePassenger, listPassengers, listCarros as fetchCarrosApi, notifyLogistics } from "../services/apiService";
|
||||
import { useData } from "../contexts/DataContext";
|
||||
|
|
@ -202,6 +202,13 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: p
|
|||
Notificação enviada em: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => window.location.href = `/painel?eventId=${agendaId}`}
|
||||
className="ml-auto flex items-center gap-2 px-3 py-1.5 bg-white border border-gray-300 rounded text-sm text-gray-700 hover:bg-gray-50 transition-colors shadow-sm"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Voltar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add Car Form - Only for Admins */}
|
||||
|
|
|
|||
|
|
@ -151,8 +151,11 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
bValue = (b as any).fotId || "";
|
||||
break;
|
||||
case "date":
|
||||
aValue = new Date(a.date + "T00:00:00").getTime();
|
||||
bValue = new Date(b.date + "T00:00:00").getTime();
|
||||
// Parse date robustly
|
||||
const dateA = a.date ? new Date(a.date.includes('T') ? a.date : a.date + "T00:00:00") : new Date(0);
|
||||
const dateB = b.date ? new Date(b.date.includes('T') ? b.date : b.date + "T00:00:00") : new Date(0);
|
||||
aValue = !isNaN(dateA.getTime()) ? dateA.getTime() : 0;
|
||||
bValue = !isNaN(dateB.getTime()) ? dateB.getTime() : 0;
|
||||
break;
|
||||
case "curso":
|
||||
aValue = ((a as any).curso || "").toLowerCase();
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onNavigate, currentPage }) => {
|
|||
{/* Logo */}
|
||||
<div
|
||||
className="flex-shrink-0 flex items-center cursor-pointer"
|
||||
onClick={() => navigate("/painel")}
|
||||
onClick={() => window.location.href = "/painel"}
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
// For now, trust DataContext + local state update.
|
||||
// Actually, DataContext refetch logic was "try import...", so it might be async.
|
||||
// Let's reload window to be 100% sure for the user as requested "mudei a data e não mudou".
|
||||
window.location.reload();
|
||||
// setView("details"); // Already called above
|
||||
// removing window.location.reload() to maintain SPA feel
|
||||
} else {
|
||||
console.error("Update function not available");
|
||||
}
|
||||
|
|
@ -89,11 +90,33 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
setView("list");
|
||||
}
|
||||
};
|
||||
const [view, setView] = useState<"list" | "create" | "edit" | "details">(
|
||||
initialView
|
||||
);
|
||||
const [view, setView] = useState<"list" | "create" | "edit" | "details">(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get("eventId") ? "details" : initialView;
|
||||
});
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
|
||||
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const eventId = params.get("eventId");
|
||||
if (eventId && events.length > 0) {
|
||||
return events.find(e => e.id === eventId) || null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Effect to sync selectedEvent if events load LATER than initial render
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const eventId = params.get("eventId");
|
||||
if (eventId && events.length > 0 && !selectedEvent) {
|
||||
const found = events.find(e => e.id === eventId);
|
||||
if (found) {
|
||||
setSelectedEvent(found);
|
||||
// Ensure view is details if we just found it
|
||||
setView("details");
|
||||
}
|
||||
}
|
||||
}, [events, window.location.search]);
|
||||
const [activeFilter, setActiveFilter] = useState<string>("all");
|
||||
const [advancedFilters, setAdvancedFilters] = useState<EventFilters>({
|
||||
date: "",
|
||||
|
|
@ -331,7 +354,16 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
};
|
||||
}, [myEvents]);
|
||||
|
||||
// Filter Logic
|
||||
// Removed the previous useEffect for eventId to avoid conflicts with the new initialization logic
|
||||
// and separate sync effect above.
|
||||
/*
|
||||
// Check for eventId in URL on mount
|
||||
useEffect(() => {
|
||||
...
|
||||
}, [events, window.location.search]);
|
||||
*/
|
||||
|
||||
// Combined filtering logic
|
||||
const filteredEvents = myEvents.filter((e) => {
|
||||
const matchesSearch = e.name
|
||||
.toLowerCase()
|
||||
|
|
@ -490,6 +522,17 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleApproveEvent = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (selectedEvent) {
|
||||
if(window.confirm("Tem certeza que deseja aprovar este evento?")) {
|
||||
await updateEventStatus(selectedEvent.id, EventStatus.CONFIRMED);
|
||||
// Optimistically update
|
||||
setSelectedEvent({ ...selectedEvent, status: EventStatus.CONFIRMED });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageTeam = () => {
|
||||
setIsTeamModalOpen(true);
|
||||
};
|
||||
|
|
@ -594,7 +637,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12 px-3 sm:px-4 lg:px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
{view === "list" && (
|
||||
{view === "list" && !new URLSearchParams(window.location.search).get("eventId") && (
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between mb-6 sm:mb-8 gap-3 sm:gap-4 fade-in">
|
||||
{renderRoleSpecificHeader()}
|
||||
{renderRoleSpecificActions()}
|
||||
|
|
@ -602,7 +645,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
)}
|
||||
|
||||
{/* Content Switcher */}
|
||||
{view === "list" && (
|
||||
{view === "list" && !new URLSearchParams(window.location.search).get("eventId") && (
|
||||
<div className="space-y-6 fade-in">
|
||||
{/* Search Bar */}
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100">
|
||||
|
|
@ -688,6 +731,15 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Loading State for Deep Link */ }
|
||||
{(!!new URLSearchParams(window.location.search).get("eventId") && (!selectedEvent || view !== "details")) && (
|
||||
<div className="flex flex-col items-center justify-center py-20 fade-in">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-purple mb-4"></div>
|
||||
<p className="text-gray-500">Carregando detalhes do evento...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === "details" && selectedEvent && (
|
||||
<div className="fade-in">
|
||||
<Button
|
||||
|
|
@ -792,6 +844,18 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
<Map size={16} className="mr-2" /> Abrir no Maps
|
||||
</Button>
|
||||
|
||||
{/* Botão de Aprovação (Apenas Admin/Owner se pendente) */}
|
||||
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) &&
|
||||
selectedEvent.status === EventStatus.PENDING_APPROVAL && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleApproveEvent}
|
||||
className="text-sm bg-green-600 text-white hover:bg-green-700 border-green-600"
|
||||
>
|
||||
<CheckCircle size={16} className="mr-2" /> Aprovar
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -29,23 +29,29 @@ const EventDetails: React.FC = () => {
|
|||
const formattedDate = new Date(event.date + "T00:00:00").toLocaleDateString();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<div className="min-h-screen bg-gray-50 p-6 pt-24">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => navigate('/eventos')} className="flex items-center gap-2 px-3 py-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-600">
|
||||
<button onClick={() => window.location.href = `/painel?eventId=${id}`} className="flex items-center gap-2 px-3 py-2 hover:bg-gray-200 rounded-lg transition-colors text-gray-600">
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span className="font-medium">Voltar</span>
|
||||
</button>
|
||||
<div className="w-px h-8 bg-gray-300 mx-2 hidden sm:block"></div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
||||
{event.empresa_nome} - {event.tipo_evento_nome}
|
||||
{(() => {
|
||||
const name = event.empresa_nome || event.name;
|
||||
const type = event.tipo_evento_nome || event.type;
|
||||
return name === type ? name : `${name} - ${type}`;
|
||||
})()}
|
||||
<span className={`text-xs px-2 py-1 rounded-full ${event.status === 'Confirmado' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
||||
{event.status}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">ID: {event.fot_id}</p>
|
||||
{(event.fot_id || event.fot || event.id) && (
|
||||
<p className="text-sm text-gray-500">ID: {event.fot_id || event.fot || event.id}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -51,12 +51,22 @@ export async function searchMapboxLocation(
|
|||
|
||||
try {
|
||||
const encodedQuery = encodeURIComponent(query);
|
||||
const url =
|
||||
let url =
|
||||
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodedQuery}.json?` +
|
||||
`access_token=${MAPBOX_TOKEN}&` +
|
||||
`country=${country}&` +
|
||||
`language=pt&` +
|
||||
`limit=5`;
|
||||
`limit=10`;
|
||||
|
||||
// Add proximity bias based on region
|
||||
const region = localStorage.getItem("photum_selected_region");
|
||||
if (region === "MG") {
|
||||
// Belo Horizonteish center
|
||||
url += `&proximity=-43.9378,-19.9208`;
|
||||
} else {
|
||||
// São Pauloish center (Default)
|
||||
url += `&proximity=-46.6333,-23.5505`;
|
||||
}
|
||||
|
||||
console.log("🔍 Buscando endereço:", query);
|
||||
const response = await fetch(url);
|
||||
|
|
|
|||
Loading…
Reference in a new issue