Merge pull request #26 from rede5/Front-back-integracao-task7
Correção da Listagem de Agenda, Filtro de Usuário e Mapeamento de Dados Resolve problemas críticos no módulo de Agenda, garantindo que os eventos sejam exibidos corretamente para o usuário logado e que as informações na interface (como o número da turma e nome do evento) correspondam aos dados reais do banco, eliminando a exibição de IDs internos e campos vazios. Alterações Realizadas 🔙 Backend Banco de Dados: Adicionada coluna user_id na tabela agenda para vincular o evento ao seu criador. Queries: CreateAgenda atualizado para salvar o user_id. Nova query ListAgendasByUser criada para filtrar os eventos pelo ID do usuário. Lógica: Atualização nos Services e Handlers para identificar o usuário via token JWT e aplicar o filtro corretamente. 🖥️ Frontend Correção de Reatividade ( DataContext ): Integração do useAuth para garantir que a lista de eventos (função fetchEvents ) seja recarregada automaticamente assim que o login é realizado. Mapeamento de Dados: FOT: Ajustado para exibir o fot_numero (ex: 25189) em vez do UUID. Nome do Evento: Criado fallback para usar o "Tipo de Evento" (ex: "Churrasco") como título caso o nome não seja informado. Formandos: Corrigido o mapeamento de qtd_formandos para attendees. Interface ( EventTable & Dashboard ): Tabelas e telas de detalhes atualizadas para usar os campos mapeados corretamente. Correção de erro de TypeScript no enum EventStatus (troca de COMPLETED para DELIVERED). Como Testar Filtro: Logar com um usuário cliente e confirmar que ele vê apenas os seus eventos. Listagem: Verificar se a coluna "FOT" mostra um número simples e se colunas como "Curso" e "Instituição" estão preenchidas. Detalhes: Abrir um evento e confirmar se "Qtd Formandos" aparece (ex: 35) e se o título do evento está correto mesmo sem observações preenchidas
This commit is contained in:
commit
52e167475c
24 changed files with 1005 additions and 370 deletions
|
|
@ -828,6 +828,14 @@ const docTemplate = `{
|
|||
"cadastro_fot"
|
||||
],
|
||||
"summary": "List all FOT records",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by Company ID",
|
||||
"name": "empresa_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
@ -2584,6 +2592,9 @@ const docTemplate = `{
|
|||
"ativo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"company_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"company_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -822,6 +822,14 @@
|
|||
"cadastro_fot"
|
||||
],
|
||||
"summary": "List all FOT records",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by Company ID",
|
||||
"name": "empresa_id",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
|
|
@ -2578,6 +2586,9 @@
|
|||
"ativo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"company_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"company_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -121,6 +121,8 @@ definitions:
|
|||
properties:
|
||||
ativo:
|
||||
type: boolean
|
||||
company_id:
|
||||
type: string
|
||||
company_name:
|
||||
type: string
|
||||
email:
|
||||
|
|
@ -963,6 +965,11 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
parameters:
|
||||
- description: Filter by Company ID
|
||||
in: query
|
||||
name: empresa_id
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,14 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
agenda, err := h.service.Create(c.Request.Context(), req)
|
||||
userIDStr := c.GetString("userID")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Usuário não autenticado"})
|
||||
return
|
||||
}
|
||||
|
||||
agenda, err := h.service.Create(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao criar agenda: " + err.Error()})
|
||||
return
|
||||
|
|
@ -52,7 +59,11 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/agenda [get]
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
agendas, err := h.service.List(c.Request.Context())
|
||||
userIDStr := c.GetString("userID")
|
||||
role := c.GetString("role")
|
||||
userID, _ := uuid.Parse(userIDStr)
|
||||
|
||||
agendas, err := h.service.List(c.Request.Context(), userID, role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erro ao listar agendas: " + err.Error()})
|
||||
return
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ func (s *Service) CalculateStatus(fotoFaltante, recepFaltante, cineFaltante int3
|
|||
return "ERRO"
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req CreateAgendaRequest) (generated.Agenda, error) {
|
||||
func (s *Service) Create(ctx context.Context, userID uuid.UUID, req CreateAgendaRequest) (generated.Agenda, error) {
|
||||
status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante)
|
||||
|
||||
params := generated.CreateAgendaParams{
|
||||
|
|
@ -86,12 +86,64 @@ func (s *Service) Create(ctx context.Context, req CreateAgendaRequest) (generate
|
|||
CineFaltante: pgtype.Int4{Int32: req.CineFaltante, Valid: true},
|
||||
LogisticaObservacoes: pgtype.Text{String: req.LogisticaObservacoes, Valid: req.LogisticaObservacoes != ""},
|
||||
PreVenda: pgtype.Bool{Bool: req.PreVenda, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
}
|
||||
|
||||
return s.queries.CreateAgenda(ctx, params)
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context) ([]generated.ListAgendasRow, error) {
|
||||
func (s *Service) List(ctx context.Context, userID uuid.UUID, role string) ([]generated.ListAgendasRow, error) {
|
||||
// If role is CLIENT (cliente), filter by userID
|
||||
if role == "cliente" || role == "EVENT_OWNER" {
|
||||
rows, err := s.queries.ListAgendasByUser(ctx, pgtype.UUID{Bytes: userID, Valid: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Convert ListAgendasByUserRow to ListAgendasRow (they are identical structurally but different types in Go)
|
||||
// To avoid manual conversion if types differ slightly by name but not structure, we might need a common interface or cast.
|
||||
// Since sqlc generates separate structs, we have to map them.
|
||||
var result []generated.ListAgendasRow
|
||||
for _, r := range rows {
|
||||
result = append(result, generated.ListAgendasRow{
|
||||
ID: r.ID,
|
||||
UserID: r.UserID,
|
||||
FotID: r.FotID,
|
||||
DataEvento: r.DataEvento,
|
||||
TipoEventoID: r.TipoEventoID,
|
||||
ObservacoesEvento: r.ObservacoesEvento,
|
||||
LocalEvento: r.LocalEvento,
|
||||
Endereco: r.Endereco,
|
||||
Horario: r.Horario,
|
||||
QtdFormandos: r.QtdFormandos,
|
||||
QtdFotografos: r.QtdFotografos,
|
||||
QtdRecepcionistas: r.QtdRecepcionistas,
|
||||
QtdCinegrafistas: r.QtdCinegrafistas,
|
||||
QtdEstudios: r.QtdEstudios,
|
||||
QtdPontoFoto: r.QtdPontoFoto,
|
||||
QtdPontoID: r.QtdPontoID,
|
||||
QtdPontoDecorado: r.QtdPontoDecorado,
|
||||
QtdPontosLed: r.QtdPontosLed,
|
||||
QtdPlataforma360: r.QtdPlataforma360,
|
||||
StatusProfissionais: r.StatusProfissionais,
|
||||
FotoFaltante: r.FotoFaltante,
|
||||
RecepFaltante: r.RecepFaltante,
|
||||
CineFaltante: r.CineFaltante,
|
||||
LogisticaObservacoes: r.LogisticaObservacoes,
|
||||
PreVenda: r.PreVenda,
|
||||
CriadoEm: r.CriadoEm,
|
||||
AtualizadoEm: r.AtualizadoEm,
|
||||
FotNumero: r.FotNumero,
|
||||
Instituicao: r.Instituicao,
|
||||
CursoNome: r.CursoNome,
|
||||
EmpresaNome: r.EmpresaNome,
|
||||
AnoSemestre: r.AnoSemestre,
|
||||
ObservacoesFot: r.ObservacoesFot,
|
||||
TipoEventoNome: r.TipoEventoNome,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
return s.queries.ListAgendas(ctx)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ type userResponse struct {
|
|||
Ativo bool `json:"ativo"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
CompanyID string `json:"company_id,omitempty"`
|
||||
CompanyName string `json:"company_name,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -186,6 +187,15 @@ func (h *Handler) Login(c *gin.Context) {
|
|||
MaxAge: 15 * 60, // 15 mins
|
||||
})
|
||||
|
||||
// Handle Nullable Fields
|
||||
var companyID, companyName string
|
||||
if user.EmpresaID.Valid {
|
||||
companyID = uuid.UUID(user.EmpresaID.Bytes).String()
|
||||
}
|
||||
if user.EmpresaNome.Valid {
|
||||
companyName = user.EmpresaNome.String
|
||||
}
|
||||
|
||||
resp := loginResponse{
|
||||
AccessToken: tokenPair.AccessToken,
|
||||
ExpiresAt: "2025-...", // logic to calculate if needed, or remove field
|
||||
|
|
@ -194,6 +204,10 @@ func (h *Handler) Login(c *gin.Context) {
|
|||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Ativo: user.Ativo,
|
||||
Name: user.Nome,
|
||||
Phone: user.Whatsapp,
|
||||
CompanyID: companyID,
|
||||
CompanyName: companyName,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -571,16 +585,22 @@ func (h *Handler) GetUser(c *gin.Context) {
|
|||
if user.EmpresaNome.Valid {
|
||||
empresaNome = user.EmpresaNome.String
|
||||
}
|
||||
var empresaID string
|
||||
if user.EmpresaID.Valid {
|
||||
empresaID = uuid.UUID(user.EmpresaID.Bytes).String()
|
||||
}
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"id": uuid.UUID(user.ID.Bytes).String(),
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
"ativo": user.Ativo,
|
||||
"created_at": user.CriadoEm.Time,
|
||||
"name": user.Nome,
|
||||
"phone": user.Whatsapp,
|
||||
"company_name": empresaNome,
|
||||
resp := loginResponse{
|
||||
User: userResponse{
|
||||
ID: uuid.UUID(user.ID.Bytes).String(),
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
Ativo: user.Ativo,
|
||||
Name: user.Nome,
|
||||
Phone: user.Whatsapp,
|
||||
CompanyID: empresaID,
|
||||
CompanyName: empresaNome,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ type TokenPair struct {
|
|||
RefreshToken string
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, *generated.Usuario, *generated.GetProfissionalByUsuarioIDRow, error) {
|
||||
func (s *Service) Login(ctx context.Context, email, senha string) (*TokenPair, *generated.GetUsuarioByEmailRow, *generated.GetProfissionalByUsuarioIDRow, error) {
|
||||
// The query now returns a Row with joined fields, not just Usuario struct
|
||||
user, err := s.queries.GetUsuarioByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.New("invalid credentials")
|
||||
|
|
|
|||
|
|
@ -128,19 +128,50 @@ func (h *Handler) Create(c *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param empresa_id query string false "Filter by Company ID"
|
||||
// @Success 200 {array} CadastroFotResponse
|
||||
// @Router /api/cadastro-fot [get]
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
empresaID := c.Query("empresa_id")
|
||||
|
||||
var response []CadastroFotResponse
|
||||
|
||||
if empresaID != "" {
|
||||
rows, err := h.service.ListByEmpresa(c.Request.Context(), empresaID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// Conversion for specific row type
|
||||
for _, r := range rows {
|
||||
response = append(response, CadastroFotResponse{
|
||||
ID: uuid.UUID(r.ID.Bytes).String(),
|
||||
Fot: r.Fot,
|
||||
EmpresaID: uuid.UUID(r.EmpresaID.Bytes).String(),
|
||||
EmpresaNome: r.EmpresaNome,
|
||||
CursoID: uuid.UUID(r.CursoID.Bytes).String(),
|
||||
CursoNome: r.CursoNome,
|
||||
AnoFormaturaID: uuid.UUID(r.AnoFormaturaID.Bytes).String(),
|
||||
AnoFormaturaLabel: r.AnoFormaturaLabel,
|
||||
Instituicao: r.Instituicao.String,
|
||||
Cidade: r.Cidade.String,
|
||||
Estado: r.Estado.String,
|
||||
Observacoes: r.Observacoes.String,
|
||||
GastosCaptacao: fromPgNumeric(r.GastosCaptacao),
|
||||
PreVenda: r.PreVenda.Bool,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
rows, err := h.service.List(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
response := []CadastroFotResponse{}
|
||||
for _, r := range rows {
|
||||
response = append(response, toListResponse(r))
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,15 @@ func (s *Service) List(ctx context.Context) ([]generated.ListCadastroFotRow, err
|
|||
return s.queries.ListCadastroFot(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) ListByEmpresa(ctx context.Context, empresaID string) ([]generated.ListCadastroFotByEmpresaRow, error) {
|
||||
uuidVal, err := uuid.Parse(empresaID)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid empresa_id")
|
||||
}
|
||||
// Note: ListCadastroFotByEmpresaRow is nearly identical to ListCadastroFotRow but we use the generated type
|
||||
return s.queries.ListCadastroFotByEmpresa(ctx, pgtype.UUID{Bytes: uuidVal, Valid: true})
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, id string) (*generated.GetCadastroFotByIDRow, error) {
|
||||
uuidVal, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ INSERT INTO agenda (
|
|||
recep_faltante,
|
||||
cine_faltante,
|
||||
logistica_observacoes,
|
||||
pre_venda
|
||||
pre_venda,
|
||||
user_id
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23
|
||||
) RETURNING 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
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24
|
||||
) 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
|
||||
`
|
||||
|
||||
type CreateAgendaParams struct {
|
||||
|
|
@ -65,6 +66,7 @@ type CreateAgendaParams struct {
|
|||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Agenda, error) {
|
||||
|
|
@ -92,10 +94,12 @@ func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Age
|
|||
arg.CineFaltante,
|
||||
arg.LogisticaObservacoes,
|
||||
arg.PreVenda,
|
||||
arg.UserID,
|
||||
)
|
||||
var i Agenda
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.FotID,
|
||||
&i.DataEvento,
|
||||
&i.TipoEventoID,
|
||||
|
|
@ -136,7 +140,7 @@ func (q *Queries) DeleteAgenda(ctx context.Context, id pgtype.UUID) error {
|
|||
}
|
||||
|
||||
const getAgenda = `-- name: GetAgenda :one
|
||||
SELECT 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 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 FROM agenda
|
||||
WHERE id = $1 LIMIT 1
|
||||
`
|
||||
|
||||
|
|
@ -145,6 +149,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error)
|
|||
var i Agenda
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.FotID,
|
||||
&i.DataEvento,
|
||||
&i.TipoEventoID,
|
||||
|
|
@ -176,7 +181,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error)
|
|||
|
||||
const listAgendas = `-- name: ListAgendas :many
|
||||
SELECT
|
||||
a.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.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,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
|
|
@ -195,6 +200,7 @@ ORDER BY a.data_evento
|
|||
|
||||
type ListAgendasRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
FotID pgtype.UUID `json:"fot_id"`
|
||||
DataEvento pgtype.Date `json:"data_evento"`
|
||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||
|
|
@ -240,6 +246,119 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) {
|
|||
var i ListAgendasRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.FotID,
|
||||
&i.DataEvento,
|
||||
&i.TipoEventoID,
|
||||
&i.ObservacoesEvento,
|
||||
&i.LocalEvento,
|
||||
&i.Endereco,
|
||||
&i.Horario,
|
||||
&i.QtdFormandos,
|
||||
&i.QtdFotografos,
|
||||
&i.QtdRecepcionistas,
|
||||
&i.QtdCinegrafistas,
|
||||
&i.QtdEstudios,
|
||||
&i.QtdPontoFoto,
|
||||
&i.QtdPontoID,
|
||||
&i.QtdPontoDecorado,
|
||||
&i.QtdPontosLed,
|
||||
&i.QtdPlataforma360,
|
||||
&i.StatusProfissionais,
|
||||
&i.FotoFaltante,
|
||||
&i.RecepFaltante,
|
||||
&i.CineFaltante,
|
||||
&i.LogisticaObservacoes,
|
||||
&i.PreVenda,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
&i.FotNumero,
|
||||
&i.Instituicao,
|
||||
&i.CursoNome,
|
||||
&i.EmpresaNome,
|
||||
&i.AnoSemestre,
|
||||
&i.ObservacoesFot,
|
||||
&i.TipoEventoNome,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
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,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
e.nome as empresa_nome,
|
||||
af.ano_semestre,
|
||||
cf.observacoes as observacoes_fot,
|
||||
te.nome as tipo_evento_nome
|
||||
FROM agenda a
|
||||
JOIN cadastro_fot cf ON a.fot_id = cf.id
|
||||
JOIN cursos c ON cf.curso_id = c.id
|
||||
JOIN empresas e ON cf.empresa_id = e.id
|
||||
JOIN anos_formaturas af ON cf.ano_formatura_id = af.id
|
||||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||
WHERE a.user_id = $1
|
||||
ORDER BY a.data_evento
|
||||
`
|
||||
|
||||
type ListAgendasByUserRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
FotID pgtype.UUID `json:"fot_id"`
|
||||
DataEvento pgtype.Date `json:"data_evento"`
|
||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
||||
LocalEvento pgtype.Text `json:"local_evento"`
|
||||
Endereco pgtype.Text `json:"endereco"`
|
||||
Horario pgtype.Text `json:"horario"`
|
||||
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
||||
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
||||
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
||||
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
||||
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
||||
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
||||
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
||||
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
||||
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
||||
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
||||
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
||||
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
||||
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
||||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||
FotNumero int32 `json:"fot_numero"`
|
||||
Instituicao pgtype.Text `json:"instituicao"`
|
||||
CursoNome string `json:"curso_nome"`
|
||||
EmpresaNome string `json:"empresa_nome"`
|
||||
AnoSemestre string `json:"ano_semestre"`
|
||||
ObservacoesFot pgtype.Text `json:"observacoes_fot"`
|
||||
TipoEventoNome string `json:"tipo_evento_nome"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([]ListAgendasByUserRow, error) {
|
||||
rows, err := q.db.Query(ctx, listAgendasByUser, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListAgendasByUserRow
|
||||
for rows.Next() {
|
||||
var i ListAgendasByUserRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.FotID,
|
||||
&i.DataEvento,
|
||||
&i.TipoEventoID,
|
||||
|
|
@ -311,7 +430,7 @@ SET
|
|||
pre_venda = $24,
|
||||
atualizado_em = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING 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
|
||||
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
|
||||
`
|
||||
|
||||
type UpdateAgendaParams struct {
|
||||
|
|
@ -371,6 +490,7 @@ func (q *Queries) UpdateAgenda(ctx context.Context, arg UpdateAgendaParams) (Age
|
|||
var i Agenda
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.FotID,
|
||||
&i.DataEvento,
|
||||
&i.TipoEventoID,
|
||||
|
|
|
|||
|
|
@ -223,6 +223,76 @@ func (q *Queries) ListCadastroFot(ctx context.Context) ([]ListCadastroFotRow, er
|
|||
return items, nil
|
||||
}
|
||||
|
||||
const listCadastroFotByEmpresa = `-- name: ListCadastroFotByEmpresa :many
|
||||
SELECT
|
||||
c.id, c.fot, c.empresa_id, c.curso_id, c.ano_formatura_id, c.instituicao, c.cidade, c.estado, c.observacoes, c.gastos_captacao, c.pre_venda, c.created_at, c.updated_at,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
FROM cadastro_fot c
|
||||
JOIN empresas e ON c.empresa_id = e.id
|
||||
JOIN cursos cur ON c.curso_id = cur.id
|
||||
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.empresa_id = $1
|
||||
ORDER BY c.fot DESC
|
||||
`
|
||||
|
||||
type ListCadastroFotByEmpresaRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Fot int32 `json:"fot"`
|
||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||
CursoID pgtype.UUID `json:"curso_id"`
|
||||
AnoFormaturaID pgtype.UUID `json:"ano_formatura_id"`
|
||||
Instituicao pgtype.Text `json:"instituicao"`
|
||||
Cidade pgtype.Text `json:"cidade"`
|
||||
Estado pgtype.Text `json:"estado"`
|
||||
Observacoes pgtype.Text `json:"observacoes"`
|
||||
GastosCaptacao pgtype.Numeric `json:"gastos_captacao"`
|
||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
EmpresaNome string `json:"empresa_nome"`
|
||||
CursoNome string `json:"curso_nome"`
|
||||
AnoFormaturaLabel string `json:"ano_formatura_label"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListCadastroFotByEmpresa(ctx context.Context, empresaID pgtype.UUID) ([]ListCadastroFotByEmpresaRow, error) {
|
||||
rows, err := q.db.Query(ctx, listCadastroFotByEmpresa, empresaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListCadastroFotByEmpresaRow
|
||||
for rows.Next() {
|
||||
var i ListCadastroFotByEmpresaRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Fot,
|
||||
&i.EmpresaID,
|
||||
&i.CursoID,
|
||||
&i.AnoFormaturaID,
|
||||
&i.Instituicao,
|
||||
&i.Cidade,
|
||||
&i.Estado,
|
||||
&i.Observacoes,
|
||||
&i.GastosCaptacao,
|
||||
&i.PreVenda,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.EmpresaNome,
|
||||
&i.CursoNome,
|
||||
&i.AnoFormaturaLabel,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateCadastroFot = `-- name: UpdateCadastroFot :one
|
||||
UPDATE cadastro_fot SET
|
||||
fot = $2,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
type Agenda struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
UserID pgtype.UUID `json:"user_id"`
|
||||
FotID pgtype.UUID `json:"fot_id"`
|
||||
DataEvento pgtype.Date `json:"data_evento"`
|
||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||
|
|
|
|||
|
|
@ -82,13 +82,35 @@ func (q *Queries) DeleteUsuario(ctx context.Context, id pgtype.UUID) error {
|
|||
}
|
||||
|
||||
const getUsuarioByEmail = `-- name: GetUsuarioByEmail :one
|
||||
SELECT id, email, senha_hash, role, ativo, criado_em, atualizado_em FROM usuarios
|
||||
WHERE email = $1 LIMIT 1
|
||||
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
|
||||
LEFT JOIN empresas e ON cc.empresa_id = e.id
|
||||
WHERE u.email = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario, error) {
|
||||
type GetUsuarioByEmailRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
SenhaHash string `json:"senha_hash"`
|
||||
Role string `json:"role"`
|
||||
Ativo bool `json:"ativo"`
|
||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||
Nome string `json:"nome"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (GetUsuarioByEmailRow, error) {
|
||||
row := q.db.QueryRow(ctx, getUsuarioByEmail, email)
|
||||
var i Usuario
|
||||
var i GetUsuarioByEmailRow
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Email,
|
||||
|
|
@ -97,6 +119,10 @@ func (q *Queries) GetUsuarioByEmail(ctx context.Context, email string) (Usuario,
|
|||
&i.Ativo,
|
||||
&i.CriadoEm,
|
||||
&i.AtualizadoEm,
|
||||
&i.Nome,
|
||||
&i.Whatsapp,
|
||||
&i.EmpresaID,
|
||||
&i.EmpresaNome,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -105,6 +131,7 @@ const getUsuarioByID = `-- name: GetUsuarioByID :one
|
|||
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
|
|
@ -123,6 +150,7 @@ type GetUsuarioByIDRow struct {
|
|||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||
Nome string `json:"nome"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +167,7 @@ func (q *Queries) GetUsuarioByID(ctx context.Context, id pgtype.UUID) (GetUsuari
|
|||
&i.AtualizadoEm,
|
||||
&i.Nome,
|
||||
&i.Whatsapp,
|
||||
&i.EmpresaID,
|
||||
&i.EmpresaNome,
|
||||
)
|
||||
return i, err
|
||||
|
|
@ -190,6 +219,7 @@ const listUsuariosPending = `-- name: ListUsuariosPending :many
|
|||
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
|
|
@ -207,6 +237,7 @@ type ListUsuariosPendingRow struct {
|
|||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||
Nome string `json:"nome"`
|
||||
Whatsapp string `json:"whatsapp"`
|
||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||
EmpresaNome pgtype.Text `json:"empresa_nome"`
|
||||
}
|
||||
|
||||
|
|
@ -227,6 +258,7 @@ func (q *Queries) ListUsuariosPending(ctx context.Context) ([]ListUsuariosPendin
|
|||
&i.CriadoEm,
|
||||
&i.Nome,
|
||||
&i.Whatsapp,
|
||||
&i.EmpresaID,
|
||||
&i.EmpresaNome,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ INSERT INTO agenda (
|
|||
recep_faltante,
|
||||
cine_faltante,
|
||||
logistica_observacoes,
|
||||
pre_venda
|
||||
pre_venda,
|
||||
user_id
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetAgenda :one
|
||||
|
|
@ -49,6 +50,25 @@ JOIN anos_formaturas af ON cf.ano_formatura_id = af.id
|
|||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||
ORDER BY a.data_evento;
|
||||
|
||||
-- name: ListAgendasByUser :many
|
||||
SELECT
|
||||
a.*,
|
||||
cf.fot as fot_numero,
|
||||
cf.instituicao,
|
||||
c.nome as curso_nome,
|
||||
e.nome as empresa_nome,
|
||||
af.ano_semestre,
|
||||
cf.observacoes as observacoes_fot,
|
||||
te.nome as tipo_evento_nome
|
||||
FROM agenda a
|
||||
JOIN cadastro_fot cf ON a.fot_id = cf.id
|
||||
JOIN cursos c ON cf.curso_id = c.id
|
||||
JOIN empresas e ON cf.empresa_id = e.id
|
||||
JOIN anos_formaturas af ON cf.ano_formatura_id = af.id
|
||||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||
WHERE a.user_id = $1
|
||||
ORDER BY a.data_evento;
|
||||
|
||||
-- name: UpdateAgenda :one
|
||||
UPDATE agenda
|
||||
SET
|
||||
|
|
|
|||
|
|
@ -17,6 +17,19 @@ JOIN cursos cur ON c.curso_id = cur.id
|
|||
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
ORDER BY c.fot DESC;
|
||||
|
||||
-- name: ListCadastroFotByEmpresa :many
|
||||
SELECT
|
||||
c.*,
|
||||
e.nome as empresa_nome,
|
||||
cur.nome as curso_nome,
|
||||
a.ano_semestre as ano_formatura_label
|
||||
FROM cadastro_fot c
|
||||
JOIN empresas e ON c.empresa_id = e.id
|
||||
JOIN cursos cur ON c.curso_id = cur.id
|
||||
JOIN anos_formaturas a ON c.ano_formatura_id = a.id
|
||||
WHERE c.empresa_id = $1
|
||||
ORDER BY c.fot DESC;
|
||||
|
||||
-- name: GetCadastroFotByID :one
|
||||
SELECT
|
||||
c.*,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,22 @@ VALUES ($1, $2, $3, false)
|
|||
RETURNING *;
|
||||
|
||||
-- name: GetUsuarioByEmail :one
|
||||
SELECT * FROM usuarios
|
||||
WHERE email = $1 LIMIT 1;
|
||||
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
LEFT JOIN cadastro_clientes cc ON u.id = cc.usuario_id
|
||||
LEFT JOIN empresas e ON cc.empresa_id = e.id
|
||||
WHERE u.email = $1 LIMIT 1;
|
||||
|
||||
-- name: GetUsuarioByID :one
|
||||
SELECT u.id, u.email, u.senha_hash, u.role, u.ativo, u.criado_em, u.atualizado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
|
|
@ -26,6 +35,7 @@ WHERE id = $1;
|
|||
SELECT u.id, u.email, u.role, u.ativo, u.criado_em,
|
||||
COALESCE(cp.nome, cc.nome, '') as nome,
|
||||
COALESCE(cp.whatsapp, cc.telefone, '') as whatsapp,
|
||||
e.id as empresa_id,
|
||||
e.nome as empresa_nome
|
||||
FROM usuarios u
|
||||
LEFT JOIN cadastro_profissionais cp ON u.id = cp.usuario_id
|
||||
|
|
|
|||
|
|
@ -316,6 +316,7 @@ CREATE TABLE IF NOT EXISTS cadastro_clientes (
|
|||
-- Agenda Table
|
||||
CREATE TABLE IF NOT EXISTS agenda (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES usuarios(id) ON DELETE CASCADE, -- User who created the event (Client/Owner)
|
||||
fot_id UUID NOT NULL REFERENCES cadastro_fot(id) ON DELETE CASCADE,
|
||||
data_evento DATE NOT NULL,
|
||||
tipo_evento_id UUID NOT NULL REFERENCES tipos_eventos(id),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { useData } from "../contexts/DataContext";
|
|||
import { UserRole } from "../types";
|
||||
import { InstitutionForm } from "./InstitutionForm";
|
||||
import { MapboxMap } from "./MapboxMap";
|
||||
import { getEventTypes, EventTypeResponse } from "../services/apiService";
|
||||
import { getEventTypes, EventTypeResponse, getCadastroFot, createAgenda } from "../services/apiService";
|
||||
|
||||
interface EventFormProps {
|
||||
onCancel: () => void;
|
||||
|
|
@ -39,12 +39,9 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
onSubmit,
|
||||
initialData,
|
||||
}) => {
|
||||
const { user } = useAuth();
|
||||
const { user, token: userToken } = useAuth();
|
||||
const {
|
||||
institutions,
|
||||
getInstitutionsByUserId,
|
||||
addInstitution,
|
||||
getActiveCoursesByInstitutionId,
|
||||
} = useData();
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"details" | "location" | "briefing" | "files"
|
||||
|
|
@ -55,19 +52,11 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
const [isGeocoding, setIsGeocoding] = useState(false);
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
|
||||
const [availableCourses, setAvailableCourses] = useState<any[]>([]);
|
||||
|
||||
const [eventTypes, setEventTypes] = useState<EventTypeResponse[]>([]);
|
||||
const [isBackendDown, setIsBackendDown] = useState(false);
|
||||
const [isLoadingEventTypes, setIsLoadingEventTypes] = useState(true);
|
||||
|
||||
// Get institutions based on user role
|
||||
// Business owners and admins see all institutions, clients see only their own
|
||||
const userInstitutions = user
|
||||
? user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN
|
||||
? institutions
|
||||
: getInstitutionsByUserId(user.id)
|
||||
: [];
|
||||
|
||||
// Default State or Initial Data
|
||||
const [formData, setFormData] = useState(
|
||||
initialData || {
|
||||
|
|
@ -78,6 +67,7 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
endTime: "",
|
||||
type: "",
|
||||
status: EventStatus.PLANNING,
|
||||
locationName: "", // New field: Nome do Local
|
||||
address: {
|
||||
street: "",
|
||||
number: "",
|
||||
|
|
@ -91,9 +81,10 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
briefing: "",
|
||||
contacts: [{ name: "", role: "", phone: "" }],
|
||||
files: [] as File[],
|
||||
institutionId: "",
|
||||
institutionId: "", // Legacy, might clear or keep for compatibility
|
||||
attendees: "",
|
||||
courseId: "",
|
||||
courseId: "", // Legacy
|
||||
fotId: "", // New field for FOT linkage
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -104,7 +95,13 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
? "Solicitar Orçamento/Evento"
|
||||
: "Cadastrar Novo Evento";
|
||||
|
||||
// Buscar tipos de eventos do backend
|
||||
const submitLabel = initialData
|
||||
? "Salvar Alterações"
|
||||
: isClientRequest
|
||||
? "Enviar Solicitação"
|
||||
: "Criar Evento";
|
||||
|
||||
// Fetch Event Types
|
||||
useEffect(() => {
|
||||
const fetchEventTypes = async () => {
|
||||
setIsLoadingEventTypes(true);
|
||||
|
|
@ -123,25 +120,63 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
|
||||
fetchEventTypes();
|
||||
}, []);
|
||||
const submitLabel = initialData
|
||||
? "Salvar Alterações"
|
||||
: isClientRequest
|
||||
? "Enviar Solicitação"
|
||||
: "Criar Evento";
|
||||
|
||||
// Carregar cursos disponíveis quando instituição for selecionada
|
||||
// Fetch FOTs filtered by user company
|
||||
const [availableFots, setAvailableFots] = useState<any[]>([]);
|
||||
const [loadingFots, setLoadingFots] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (formData.institutionId) {
|
||||
const courses = getActiveCoursesByInstitutionId(formData.institutionId);
|
||||
setAvailableCourses(courses);
|
||||
} else {
|
||||
setAvailableCourses([]);
|
||||
// Limpa o curso selecionado se a instituição mudar
|
||||
if (formData.courseId) {
|
||||
setFormData((prev: any) => ({ ...prev, courseId: "" }));
|
||||
const loadFots = async () => {
|
||||
// Allow FOT loading for Business Owners, Event Owners (Clients), and Superadmins
|
||||
if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.EVENT_OWNER || user?.role === UserRole.SUPERADMIN) {
|
||||
// If user is not superadmin (admin generally has no empresaId but sees all or selects one, here we assume superadmin logic is separate or allowed)
|
||||
// Check if regular user has empresaId
|
||||
if (user?.role !== UserRole.SUPERADMIN && !user?.empresaId) {
|
||||
// If no company linked, do not load FOTs
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingFots(true);
|
||||
const token = localStorage.getItem("token") || "";
|
||||
// Use empresaId from user context if available
|
||||
const empresaId = user.empresaId;
|
||||
const response = await getCadastroFot(token, empresaId);
|
||||
|
||||
if (response.data) {
|
||||
// If we didn't filter by API (e.g. no empresaId), filter client side as fallback
|
||||
const myFots = (empresaId || user.companyName)
|
||||
? response.data.filter(f =>
|
||||
(empresaId && f.empresa_id === empresaId) ||
|
||||
(user.companyName && f.empresa_nome === user.companyName)
|
||||
)
|
||||
: response.data;
|
||||
|
||||
setAvailableFots(myFots);
|
||||
}
|
||||
}, [formData.institutionId, getActiveCoursesByInstitutionId]);
|
||||
setLoadingFots(false);
|
||||
}
|
||||
};
|
||||
loadFots();
|
||||
}, [user]);
|
||||
|
||||
// Derived state for dropdowns
|
||||
const [selectedCourseName, setSelectedCourseName] = useState("");
|
||||
const [selectedInstitutionName, setSelectedInstitutionName] = useState("");
|
||||
|
||||
// Unique Courses
|
||||
const uniqueCourses = Array.from(new Set(availableFots.map(f => f.curso_nome))).sort();
|
||||
|
||||
// Filtered Institutions based on Course
|
||||
const filteredInstitutions = availableFots
|
||||
.filter(f => f.curso_nome === selectedCourseName)
|
||||
.map(f => f.instituicao)
|
||||
.filter((v, i, a) => a.indexOf(v) === i)
|
||||
.sort();
|
||||
|
||||
// Filtered Years based on Course + Inst
|
||||
const filteredYears = availableFots
|
||||
.filter(f => f.curso_nome === selectedCourseName && f.instituicao === selectedInstitutionName)
|
||||
.map(f => ({ id: f.id, label: f.ano_formatura_label }));
|
||||
|
||||
// Address Autocomplete Logic using Mapbox
|
||||
useEffect(() => {
|
||||
|
|
@ -177,7 +212,6 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
};
|
||||
|
||||
const handleMapLocationChange = async (lat: number, lng: number) => {
|
||||
// Buscar endereço baseado nas coordenadas
|
||||
const addressData = await reverseGeocode(lat, lng);
|
||||
|
||||
if (addressData) {
|
||||
|
|
@ -195,7 +229,6 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
},
|
||||
}));
|
||||
} else {
|
||||
// Se não conseguir o endereço, atualiza apenas as coordenadas
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
|
|
@ -208,23 +241,16 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// Geocoding quando o usuário digita o endereço manualmente
|
||||
const handleManualAddressChange = async () => {
|
||||
const { street, number, city, state } = formData.address;
|
||||
|
||||
// Montar query de busca
|
||||
const query = `${street} ${number}, ${city}, ${state}`.trim();
|
||||
|
||||
if (query.length < 5) return; // Endereço muito curto
|
||||
if (query.length < 5) return;
|
||||
|
||||
setIsGeocoding(true);
|
||||
|
||||
try {
|
||||
const results = await searchMapboxLocation(query);
|
||||
|
||||
if (results.length > 0) {
|
||||
const firstResult = results[0];
|
||||
|
||||
setFormData((prev: any) => ({
|
||||
...prev,
|
||||
address: {
|
||||
|
|
@ -264,25 +290,72 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Validate institution selection
|
||||
if (!formData.institutionId) {
|
||||
alert("Por favor, selecione uma instituição antes de continuar.");
|
||||
const handleSubmit = async () => {
|
||||
// Validation
|
||||
if (!formData.name) return alert("Preencha o tipo de evento");
|
||||
if (!formData.date) return alert("Preencha a data");
|
||||
if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.EVENT_OWNER) {
|
||||
if (!formData.fotId) {
|
||||
alert("Por favor, selecione a Turma (Cadastro FOT) antes de continuar.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate course selection
|
||||
if (!formData.courseId) {
|
||||
alert("Por favor, selecione um curso/turma antes de continuar.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Show toast
|
||||
try {
|
||||
setShowToast(true);
|
||||
// Call original submit after small delay for visual effect or immediately
|
||||
|
||||
// Prepare Payload for Agenda API
|
||||
const payload = {
|
||||
fot_id: formData.fotId,
|
||||
tipo_evento_id: formData.typeId || "00000000-0000-0000-0000-000000000000",
|
||||
data_evento: new Date(formData.date).toISOString(),
|
||||
horario: formData.startTime || "",
|
||||
observacoes_evento: formData.briefing || "",
|
||||
local_evento: formData.locationName || "",
|
||||
endereco: `${formData.address.street}, ${formData.address.number} - ${formData.address.city}/${formData.address.state}`,
|
||||
qtd_formandos: parseInt(formData.attendees) || 0,
|
||||
|
||||
// Default integer values
|
||||
qtd_fotografos: 0,
|
||||
qtd_recepcionistas: 0,
|
||||
qtd_cinegrafistas: 0,
|
||||
qtd_estudios: 0,
|
||||
qtd_ponto_foto: 0,
|
||||
qtd_ponto_id: 0,
|
||||
qtd_ponto_decorado: 0,
|
||||
qtd_pontos_led: 0,
|
||||
qtd_plataforma_360: 0,
|
||||
|
||||
status_profissionais: "PENDING",
|
||||
foto_faltante: 0,
|
||||
recep_faltante: 0,
|
||||
cine_faltante: 0,
|
||||
logistica_observacoes: "",
|
||||
pre_venda: true
|
||||
};
|
||||
|
||||
const authToken = userToken || localStorage.getItem("token") || "";
|
||||
const response = await createAgenda(authToken, payload);
|
||||
|
||||
if (response.error) {
|
||||
alert("Erro ao criar evento: " + response.error);
|
||||
setShowToast(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (onSubmit) {
|
||||
onSubmit(formData);
|
||||
}
|
||||
// Redirect or close is handled by parent, but we show success via toast usually
|
||||
alert("Solicitação enviada com sucesso!");
|
||||
}, 1000);
|
||||
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
alert("Erro inesperado: " + e.message);
|
||||
setShowToast(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstitutionSubmit = (institutionData: any) => {
|
||||
|
|
@ -292,11 +365,10 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
ownerId: user?.id || "",
|
||||
};
|
||||
addInstitution(newInstitution);
|
||||
setFormData((prev) => ({ ...prev, institutionId: newInstitution.id }));
|
||||
setFormData((prev: any) => ({ ...prev, institutionId: newInstitution.id }));
|
||||
setShowInstitutionForm(false);
|
||||
};
|
||||
|
||||
// Show institution form modal
|
||||
if (showInstitutionForm) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
|
|
@ -337,7 +409,7 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
: "Preencha as informações técnicas do evento."}
|
||||
</p>
|
||||
</div>
|
||||
{/* Step indicators - Hidden on mobile, shown on tablet+ */}
|
||||
{/* Step indicators */}
|
||||
<div className="hidden sm:flex space-x-2">
|
||||
{["details", "location", "briefing", "files"].map((tab, idx) => (
|
||||
<div
|
||||
|
|
@ -359,7 +431,7 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Tabs - Horizontal */}
|
||||
{/* Mobile Tabs */}
|
||||
<div className="lg:hidden border-b border-gray-200 bg-white overflow-x-auto scrollbar-hide">
|
||||
<div className="flex min-w-max">
|
||||
{[
|
||||
|
|
@ -380,11 +452,9 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
: "text-gray-500 border-transparent hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-5 h-5 rounded-full text-[10px] leading-5 text-center mr-1.5 ${
|
||||
<span className="inline-block w-5 h-5 rounded-full text-[10px] leading-5 text-center mr-1.5 ${
|
||||
activeTab === item.id ? 'bg-brand-gold text-white' : 'bg-gray-200 text-gray-600'
|
||||
}"
|
||||
>
|
||||
}">
|
||||
{item.icon}
|
||||
</span>
|
||||
{item.label}
|
||||
|
|
@ -429,14 +499,22 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
</label>
|
||||
<select
|
||||
required
|
||||
value={formData.type}
|
||||
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||
value={formData.typeId || ""}
|
||||
onChange={(e) => {
|
||||
const selectedId = e.target.value;
|
||||
const selectedType = eventTypes.find((t) => t.id === selectedId);
|
||||
setFormData({
|
||||
...formData,
|
||||
typeId: selectedId,
|
||||
type: selectedType?.nome || ""
|
||||
});
|
||||
}}
|
||||
disabled={isLoadingEventTypes || isBackendDown}
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
>
|
||||
<option value="">Selecione o tipo de evento</option>
|
||||
{eventTypes.map((type) => (
|
||||
<option key={type.id} value={type.nome}>
|
||||
<option key={type.id} value={type.id}>
|
||||
{type.nome}
|
||||
</option>
|
||||
))}
|
||||
|
|
@ -455,8 +533,8 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
</div>
|
||||
|
||||
<Input
|
||||
label="Nome do Evento (Opcional)"
|
||||
placeholder="Ex: Formatura Educação Física 2025"
|
||||
label="Observações do Evento (Opcional)"
|
||||
placeholder="Ex: Cerimônia de Colação de Grau"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
|
|
@ -482,23 +560,21 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
required
|
||||
/>
|
||||
<Input
|
||||
label="Horário de Término*"
|
||||
label="Horário de Término (Opcional)"
|
||||
type="time"
|
||||
value={formData.endTime}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, endTime: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
label="Número de universitários"
|
||||
placeholder="Ex: 150"
|
||||
label="Número de Formandos"
|
||||
placeholder="Ex: 50"
|
||||
value={formData.attendees}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// Permite apenas números
|
||||
if (value === "" || /^\d+$/.test(value)) {
|
||||
setFormData({ ...formData, attendees: value });
|
||||
}
|
||||
|
|
@ -507,137 +583,85 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
inputMode="numeric"
|
||||
/>
|
||||
|
||||
{/* Institution Selection - OBRIGATÓRIO */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
Universidade*{" "}
|
||||
<span className="text-brand-gold">(Obrigatório)</span>
|
||||
</label>
|
||||
{/* Dynamic FOT Selection */}
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-4 uppercase tracking-wider">Seleção da Turma</h3>
|
||||
|
||||
{userInstitutions.length === 0 ? (
|
||||
<div className="border-2 border-dashed border-amber-300 bg-amber-50 rounded-sm p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle
|
||||
className="text-amber-600 flex-shrink-0 mt-0.5"
|
||||
size={20}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-amber-900 mb-2">
|
||||
Nenhuma universidade cadastrada
|
||||
</p>
|
||||
<p className="text-xs text-amber-700 mb-3">
|
||||
Você precisa cadastrar uma universidade antes de
|
||||
criar um evento. Trabalhamos exclusivamente com
|
||||
eventos fotográficos em universidades.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstitutionForm(true)}
|
||||
className="text-xs font-bold text-amber-900 hover:text-amber-700 underline flex items-center"
|
||||
>
|
||||
<Plus size={14} className="mr-1" />
|
||||
Cadastrar minha primeira universidade
|
||||
</button>
|
||||
{!user?.empresaId && user?.role !== UserRole.SUPERADMIN ? (
|
||||
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<AlertCircle className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<select
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors"
|
||||
value={formData.institutionId}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
institutionId: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
>
|
||||
<option value="">Selecione uma universidade</option>
|
||||
{userInstitutions.map((inst) => (
|
||||
<option key={inst.id} value={inst.id}>
|
||||
{inst.name} - {inst.type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowInstitutionForm(true)}
|
||||
className="text-xs text-brand-gold hover:underline flex items-center"
|
||||
>
|
||||
<Plus size={12} className="mr-1" />
|
||||
Cadastrar nova universidade
|
||||
</button>
|
||||
|
||||
{formData.institutionId && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-sm p-3 flex items-center">
|
||||
<Check size={16} className="text-green-600 mr-2" />
|
||||
<span className="text-xs text-green-800">
|
||||
Universidade selecionada com sucesso
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Course Selection - Condicional baseado na instituição */}
|
||||
{formData.institutionId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
Curso/Turma <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
{availableCourses.length === 0 ? (
|
||||
<div className="border-2 border-dashed border-gray-300 bg-gray-50 rounded-sm p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertCircle
|
||||
className="text-gray-400 flex-shrink-0 mt-0.5"
|
||||
size={20}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-700 mb-1">
|
||||
Nenhum curso cadastrado
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Entre em contato com a administração para
|
||||
cadastrar os cursos/turmas disponíveis nesta
|
||||
universidade.
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-red-700">
|
||||
Sua conta não está vinculada a nenhuma empresa. Por favor, entre em contato com a administração para regularizar seu cadastro antes de solicitar um evento.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 1. Curso */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm text-gray-600 mb-1">Curso</label>
|
||||
<select
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors"
|
||||
value={formData.courseId}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
courseId: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
||||
value={selectedCourseName}
|
||||
onChange={e => {
|
||||
setSelectedCourseName(e.target.value);
|
||||
setSelectedInstitutionName("");
|
||||
setFormData({ ...formData, fotId: "" });
|
||||
}}
|
||||
disabled={loadingFots}
|
||||
>
|
||||
<option value="">Selecione um curso *</option>
|
||||
{availableCourses.map((course) => (
|
||||
<option key={course.id} value={course.id}>
|
||||
{course.name} - {course.graduationType} (
|
||||
{course.year}/{course.semester}º sem)
|
||||
</option>
|
||||
<option value="">Selecione o Curso</option>
|
||||
{uniqueCourses.map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 2. Instituição */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm text-gray-600 mb-1">Instituição</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
||||
value={selectedInstitutionName}
|
||||
onChange={e => {
|
||||
setSelectedInstitutionName(e.target.value);
|
||||
setFormData({ ...formData, fotId: "" });
|
||||
}}
|
||||
disabled={!selectedCourseName}
|
||||
>
|
||||
<option value="">Selecione a Instituição</option>
|
||||
{filteredInstitutions.map(i => <option key={i} value={i}>{i}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 3. Ano/Turma (Final FOT Selection) */}
|
||||
<div className="mb-0">
|
||||
<label className="block text-sm text-gray-600 mb-1">Ano/Turma</label>
|
||||
<select
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
||||
value={formData.fotId || ""}
|
||||
onChange={e => setFormData({ ...formData, fotId: e.target.value })}
|
||||
disabled={!selectedInstitutionName}
|
||||
>
|
||||
<option value="">Selecione a Turma</option>
|
||||
{filteredYears.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0 mt-8">
|
||||
<Button
|
||||
onClick={() => setActiveTab("location")}
|
||||
className="w-full sm:w-auto"
|
||||
disabled={(!user?.empresaId && user?.role !== UserRole.SUPERADMIN)}
|
||||
>
|
||||
Próximo: Localização
|
||||
</Button>
|
||||
|
|
@ -647,8 +671,17 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
|
||||
{activeTab === "location" && (
|
||||
<div className="space-y-6 fade-in">
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
|
||||
|
||||
{/* Nome do Local */}
|
||||
<Input
|
||||
label="Nome do Local"
|
||||
placeholder="Ex: Espaço das Américas, Salão de Festas X"
|
||||
value={formData.locationName}
|
||||
onChange={(e) => setFormData({ ...formData, locationName: e.target.value })}
|
||||
/>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2 tracking-wide uppercase text-xs">
|
||||
Busca de Endereço (Powered by Mapbox)
|
||||
</label>
|
||||
<div className="relative">
|
||||
|
|
@ -976,7 +1009,7 @@ export const EventForm: React.FC<EventFormProps> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
[EventStatus.CONFIRMED]: "Confirmado",
|
||||
[EventStatus.PLANNING]: "Planejamento",
|
||||
[EventStatus.IN_PROGRESS]: "Em Andamento",
|
||||
[EventStatus.COMPLETED]: "Concluído",
|
||||
[EventStatus.DELIVERED]: "Entregue",
|
||||
[EventStatus.ARCHIVED]: "Arquivado",
|
||||
};
|
||||
return statusLabels[status] || status;
|
||||
|
|
@ -232,7 +232,7 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{(event as any).fotId || "-"}
|
||||
{event.fot || "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
|
|
@ -242,22 +242,22 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
{(event as any).curso || "-"}
|
||||
{event.curso || "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
{(event as any).instituicao || "-"}
|
||||
{event.instituicao || "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
{(event as any).anoFormatura || "-"}
|
||||
{event.anoFormatura || "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-gray-600">
|
||||
{(event as any).empresa || "-"}
|
||||
{event.empresa || "-"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
|
|
@ -265,8 +265,7 @@ export const EventTable: React.FC<EventTableProps> = ({
|
|||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
STATUS_COLORS[event.status] || "bg-gray-100 text-gray-800"
|
||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${STATUS_COLORS[event.status] || "bg-gray-100 text-gray-800"
|
||||
}`}
|
||||
>
|
||||
{getStatusDisplay(event.status)}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
name: backendUser.email.split('@')[0],
|
||||
role: backendUser.role as UserRole,
|
||||
ativo: backendUser.ativo,
|
||||
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
|
||||
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
|
||||
};
|
||||
if (!backendUser.ativo) {
|
||||
console.warn("User is not active, logging out.");
|
||||
|
|
@ -155,6 +157,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
name: backendUser.email.split('@')[0], // Fallback name or from profile if available
|
||||
role: backendUser.role as UserRole,
|
||||
ativo: backendUser.ativo,
|
||||
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
|
||||
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
|
||||
// ... propagate other fields if needed or fetch profile
|
||||
};
|
||||
|
||||
|
|
@ -227,6 +231,8 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||
name: backendUser.nome || backendUser.email.split('@')[0],
|
||||
role: backendUser.role as UserRole,
|
||||
ativo: backendUser.ativo,
|
||||
empresaId: backendUser.company_id || backendUser.empresa_id || backendUser.companyId,
|
||||
companyName: backendUser.company_name || backendUser.empresa_nome || backendUser.companyName,
|
||||
};
|
||||
setUser(mappedUser);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { createContext, useContext, useState, ReactNode, useEffect } from "react";
|
||||
import { useAuth } from "./AuthContext";
|
||||
import { getPendingUsers, approveUser as apiApproveUser } from "../services/apiService";
|
||||
import {
|
||||
EventData,
|
||||
|
|
@ -607,15 +608,75 @@ interface DataContextType {
|
|||
|
||||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||
|
||||
|
||||
export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { token, user } = useAuth(); // Consume Auth Context
|
||||
const [events, setEvents] = useState<EventData[]>(INITIAL_EVENTS);
|
||||
const [institutions, setInstitutions] =
|
||||
useState<Institution[]>(INITIAL_INSTITUTIONS);
|
||||
const [courses, setCourses] = useState<Course[]>(INITIAL_COURSES);
|
||||
const [pendingUsers, setPendingUsers] = useState<User[]>([]);
|
||||
|
||||
// Fetch events from API
|
||||
useEffect(() => {
|
||||
const fetchEvents = async () => {
|
||||
// Use token from context or fallback to localStorage if context not ready (though context is preferred sources of truth)
|
||||
const visibleToken = token || localStorage.getItem("token");
|
||||
|
||||
if (visibleToken) {
|
||||
try {
|
||||
// Import dynamic to avoid circular dependency if any, or just use imported service
|
||||
const { getAgendas } = await import("../services/apiService");
|
||||
const result = await getAgendas(visibleToken);
|
||||
if (result.data) {
|
||||
const mappedEvents: EventData[] = result.data.map((e: any) => ({
|
||||
id: e.id,
|
||||
name: e.observacoes_evento || e.tipo_evento_nome || "Evento sem nome", // Fallback mapping
|
||||
date: e.data_evento ? e.data_evento.split('T')[0] : "",
|
||||
time: e.horario || "00:00",
|
||||
type: (e.tipo_evento_nome || "Outro") as EventType, // Map string to enum if possible, or keep string
|
||||
status: EventStatus.PENDING_APPROVAL, // Default or map from e.status_profissionais?? e.status_profissionais is for workers. We might need a status field on agenda table or infer it.
|
||||
// For now, let's map status_profissionais to general status if possible, or default to CONFIRMED/PENDING
|
||||
// e.status_profissionais defaults to "PENDING" in creation.
|
||||
address: {
|
||||
street: e.endereco ? e.endereco.split(',')[0] : "",
|
||||
number: e.endereco ? e.endereco.split(',')[1]?.split('-')[0]?.trim() || "" : "",
|
||||
city: e.endereco ? e.endereco.split('-')[1]?.split('/')[0]?.trim() || "" : "",
|
||||
state: e.endereco ? e.endereco.split('/')[1]?.trim() || "" : "",
|
||||
zip: "",
|
||||
mapLink: e.local_evento.startsWith('http') ? e.local_evento : undefined
|
||||
},
|
||||
briefing: e.observacoes_evento || "",
|
||||
coverImage: "https://picsum.photos/id/10/800/400", // Placeholder
|
||||
contacts: [], // TODO: fetch contacts if needed
|
||||
checklist: [],
|
||||
ownerId: e.user_id || "unknown",
|
||||
photographerIds: [], // TODO
|
||||
institutionId: "", // TODO
|
||||
attendees: e.qtd_formandos,
|
||||
fotId: e.fot_id, // UUID
|
||||
|
||||
// Joined Fields
|
||||
fot: e.fot_numero ?? e.fot_id, // Show Number if available (even 0), else ID
|
||||
curso: e.curso_nome,
|
||||
instituicao: e.instituicao,
|
||||
anoFormatura: e.ano_semestre,
|
||||
empresa: e.empresa_nome,
|
||||
observacoes: e.observacoes_fot,
|
||||
typeId: e.tipo_evento_id
|
||||
}));
|
||||
setEvents(mappedEvents);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch events", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchEvents();
|
||||
}, [token]); // React to token change
|
||||
|
||||
// Fetch pending users from API
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
|
|
@ -653,8 +714,60 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
|||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const addEvent = (event: EventData) => {
|
||||
const addEvent = async (event: EventData) => {
|
||||
const token = localStorage.getItem("@Photum:token");
|
||||
if (!token) {
|
||||
console.error("No token found");
|
||||
// Fallback for offline/mock
|
||||
setEvents((prev) => [event, ...prev]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Map frontend fields (camelCase) to backend fields (snake_case)
|
||||
const payload = {
|
||||
fot_id: event.fotId,
|
||||
data_evento: event.date, // "YYYY-MM-DD" is acceptable
|
||||
tipo_evento_id: event.typeId,
|
||||
observacoes_evento: event.name, // "Observações do Evento" maps to name in EventForm
|
||||
// local_evento: event.address.street + ", " + event.address.number, // Or map separate fields if needed
|
||||
local_evento: event.address.mapLink || "Local a definir", // using mapLink or some string
|
||||
endereco: `${event.address.street}, ${event.address.number}, ${event.address.city} - ${event.address.state}`,
|
||||
horario: event.startTime,
|
||||
// Defaulting missing counts to 0 for now as they are not in the simplified form
|
||||
qtd_formandos: event.attendees ? parseInt(String(event.attendees)) : 0,
|
||||
qtd_fotografos: 0,
|
||||
qtd_recepcionistas: 0,
|
||||
qtd_cinegrafistas: 0,
|
||||
qtd_estudios: 0,
|
||||
qtd_ponto_foto: 0,
|
||||
qtd_ponto_id: 0,
|
||||
qtd_ponto_decorado: 0,
|
||||
qtd_pontos_led: 0,
|
||||
qtd_plataforma_360: 0,
|
||||
status_profissionais: "AGUARDANDO", // Will be calculated by backend anyway
|
||||
foto_faltante: 0,
|
||||
recep_faltante: 0,
|
||||
cine_faltante: 0,
|
||||
logistica_observacoes: "",
|
||||
pre_venda: false
|
||||
};
|
||||
|
||||
const result = await import("../services/apiService").then(m => m.createAgenda(token, payload));
|
||||
|
||||
if (result.data) {
|
||||
// Success
|
||||
console.log("Agenda criada:", result.data);
|
||||
const newEvent = { ...event, id: result.data.id, status: EventStatus.PENDING_APPROVAL };
|
||||
setEvents((prev) => [newEvent, ...prev]);
|
||||
} else {
|
||||
console.error("Erro ao criar agenda API:", result.error);
|
||||
// Fallback or Toast?
|
||||
// We will optimistically add it locally or throw
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Exception creating agenda:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const updateEventStatus = (id: string, status: EventStatus) => {
|
||||
|
|
|
|||
|
|
@ -371,8 +371,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
<div className="flex space-x-2 bg-white p-1 rounded border border-gray-200">
|
||||
<button
|
||||
onClick={() => setActiveFilter("all")}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-sm ${
|
||||
activeFilter === "all"
|
||||
className={`px-3 py-1 text-xs font-medium rounded-sm ${activeFilter === "all"
|
||||
? "bg-brand-black text-white"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
|
|
@ -381,8 +380,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
</button>
|
||||
<button
|
||||
onClick={() => setActiveFilter("pending")}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-sm flex items-center ${
|
||||
activeFilter === "pending"
|
||||
className={`px-3 py-1 text-xs font-medium rounded-sm flex items-center ${activeFilter === "pending"
|
||||
? "bg-brand-gold text-white"
|
||||
: "text-gray-600 hover:bg-gray-100"
|
||||
}`}
|
||||
|
|
@ -481,8 +479,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`px-4 py-2 rounded text-sm font-semibold ${
|
||||
STATUS_COLORS[selectedEvent.status]
|
||||
className={`px-4 py-2 rounded text-sm font-semibold ${STATUS_COLORS[selectedEvent.status]
|
||||
}`}
|
||||
>
|
||||
{selectedEvent.status}
|
||||
|
|
@ -578,7 +575,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
FOT
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 font-medium">
|
||||
{(selectedEvent as any).fotId || "-"}
|
||||
{(selectedEvent as any).fot || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -596,7 +593,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Curso
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).curso || "-"}
|
||||
{selectedEvent.curso || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -604,7 +601,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Instituição
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).instituicao || "-"}
|
||||
{selectedEvent.instituicao || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -612,7 +609,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Ano Formatura
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).anoFormatura || "-"}
|
||||
{selectedEvent.anoFormatura || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -620,7 +617,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Empresa
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).empresa || "-"}
|
||||
{selectedEvent.empresa || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-gray-50">
|
||||
|
|
@ -672,7 +669,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
Qtd Formandos
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900">
|
||||
{(selectedEvent as any).qtdFormandos || "-"}
|
||||
{selectedEvent.attendees || "-"}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -866,8 +863,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
<div
|
||||
className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
photographer?.avatar ||
|
||||
backgroundImage: `url(${photographer?.avatar ||
|
||||
`https://i.pravatar.cc/100?u=${id}`
|
||||
})`,
|
||||
backgroundSize: "cover",
|
||||
|
|
@ -1034,8 +1030,7 @@ export const Dashboard: React.FC<DashboardProps> = ({
|
|||
togglePhotographer(photographer.id)
|
||||
}
|
||||
disabled={!isAvailable && !isAssigned}
|
||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${
|
||||
isAssigned
|
||||
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${isAssigned
|
||||
? "bg-red-100 text-red-700 hover:bg-red-200"
|
||||
: isAvailable
|
||||
? "bg-brand-gold text-white hover:bg-[#a5bd2e]"
|
||||
|
|
|
|||
|
|
@ -177,9 +177,17 @@ export async function getAvailableCourses(): Promise<ApiResponse<Array<{ id: str
|
|||
/**
|
||||
* Busca a listagem de Cadastro FOT
|
||||
*/
|
||||
export async function getCadastroFot(token: string): Promise<ApiResponse<any[]>> {
|
||||
/**
|
||||
* Busca a listagem de Cadastro FOT
|
||||
*/
|
||||
export async function getCadastroFot(token: string, empresaId?: string): Promise<ApiResponse<any[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/cadastro-fot`, {
|
||||
let url = `${API_BASE_URL}/api/cadastro-fot`;
|
||||
if (empresaId) {
|
||||
url += `?empresa_id=${empresaId}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -270,7 +278,55 @@ export async function getUniversities(): Promise<
|
|||
return fetchFromBackend("/api/universidades");
|
||||
}
|
||||
|
||||
// ... existing functions ...
|
||||
// Agenda
|
||||
export const createAgenda = async (token: string, data: any) => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agenda`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return { data: responseData, error: null };
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao criar agenda:", error);
|
||||
return { data: null, error: error.message || "Erro ao criar agenda" };
|
||||
}
|
||||
};
|
||||
|
||||
// Agenda
|
||||
export const getAgendas = async (token: string): Promise<ApiResponse<any[]>> => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/agenda`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { data, error: null, isBackendDown: false };
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao buscar agendas:", error);
|
||||
return { data: null, error: error.message || "Erro ao buscar agendas", isBackendDown: true };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Busca usuários pendentes de aprovação
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export interface User {
|
|||
phone?: string; // Telefone do usuário
|
||||
createdAt?: string; // Data de criação do cadastro
|
||||
ativo?: boolean;
|
||||
empresaId?: string; // ID da empresa vinculada (para Business Owners)
|
||||
companyName?: string; // Nome da empresa vinculada
|
||||
}
|
||||
|
||||
export interface Institution {
|
||||
|
|
@ -115,4 +117,15 @@ export interface EventData {
|
|||
institutionId?: string; // ID da instituição vinculada (obrigatório)
|
||||
attendees?: number; // Número de pessoas participantes
|
||||
courseId?: string; // ID do curso/turma relacionado ao evento
|
||||
fotId?: string; // ID da Turma (FOT)
|
||||
typeId?: string; // ID do Tipo de Evento (UUID)
|
||||
|
||||
// Fields populated from backend joins (ListAgendas)
|
||||
fot?: string; // Nome/Número da Turma (FOT)
|
||||
curso?: string; // Nome do Curso
|
||||
instituicao?: string; // Nome da Instituição
|
||||
anoFormatura?: string; // Ano/Semestre
|
||||
empresa?: string; // Nome da Empresa
|
||||
observacoes?: string; // Observações da FOT
|
||||
tipoEventoNome?: string; // Nome do Tipo de Evento
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue