diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 3979b8f..efa92bf 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -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" }, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index e87efe4..38ff923 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -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" }, diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 59cd3b1..7e4b3f0 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -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: diff --git a/backend/internal/agenda/handler.go b/backend/internal/agenda/handler.go index 48cdb85..e8fab06 100644 --- a/backend/internal/agenda/handler.go +++ b/backend/internal/agenda/handler.go @@ -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 diff --git a/backend/internal/agenda/service.go b/backend/internal/agenda/service.go index 6de4464..0db2174 100644 --- a/backend/internal/agenda/service.go +++ b/backend/internal/agenda/service.go @@ -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) } diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 66615fa..6c505c7 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -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,14 +187,27 @@ 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 User: userResponse{ - ID: uuid.UUID(user.ID.Bytes).String(), - Email: user.Email, - Role: user.Role, - Ativo: user.Ativo, + ID: uuid.UUID(user.ID.Bytes).String(), + 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) diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 34e2b05..e1e41ba 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -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") diff --git a/backend/internal/cadastro_fot/handler.go b/backend/internal/cadastro_fot/handler.go index 1e3287b..1242be3 100644 --- a/backend/internal/cadastro_fot/handler.go +++ b/backend/internal/cadastro_fot/handler.go @@ -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) { - rows, err := h.service.List(c.Request.Context()) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + 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 + } + for _, r := range rows { + response = append(response, toListResponse(r)) + } } - response := []CadastroFotResponse{} - for _, r := range rows { - response = append(response, toListResponse(r)) - } c.JSON(http.StatusOK, response) } diff --git a/backend/internal/cadastro_fot/service.go b/backend/internal/cadastro_fot/service.go index dd788f6..4a8f4a0 100644 --- a/backend/internal/cadastro_fot/service.go +++ b/backend/internal/cadastro_fot/service.go @@ -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 { diff --git a/backend/internal/db/generated/agenda.sql.go b/backend/internal/db/generated/agenda.sql.go index f034fd9..6295176 100644 --- a/backend/internal/db/generated/agenda.sql.go +++ b/backend/internal/db/generated/agenda.sql.go @@ -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, diff --git a/backend/internal/db/generated/cadastro_fot.sql.go b/backend/internal/db/generated/cadastro_fot.sql.go index 9da10df..d720733 100644 --- a/backend/internal/db/generated/cadastro_fot.sql.go +++ b/backend/internal/db/generated/cadastro_fot.sql.go @@ -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, diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index a211a08..558b88f 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -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"` diff --git a/backend/internal/db/generated/usuarios.sql.go b/backend/internal/db/generated/usuarios.sql.go index 6695e97..2e50934 100644 --- a/backend/internal/db/generated/usuarios.sql.go +++ b/backend/internal/db/generated/usuarios.sql.go @@ -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 diff --git a/backend/internal/db/queries/agenda.sql b/backend/internal/db/queries/agenda.sql index 7f6809e..a5c54e5 100644 --- a/backend/internal/db/queries/agenda.sql +++ b/backend/internal/db/queries/agenda.sql @@ -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 diff --git a/backend/internal/db/queries/cadastro_fot.sql b/backend/internal/db/queries/cadastro_fot.sql index 23918aa..4d923fa 100644 --- a/backend/internal/db/queries/cadastro_fot.sql +++ b/backend/internal/db/queries/cadastro_fot.sql @@ -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.*, diff --git a/backend/internal/db/queries/usuarios.sql b/backend/internal/db/queries/usuarios.sql index 44344cc..103989f 100644 --- a/backend/internal/db/queries/usuarios.sql +++ b/backend/internal/db/queries/usuarios.sql @@ -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 diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 9623138..a57877e 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -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), diff --git a/frontend/components/EventForm.tsx b/frontend/components/EventForm.tsx index ae180be..82313ae 100644 --- a/frontend/components/EventForm.tsx +++ b/frontend/components/EventForm.tsx @@ -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 = ({ 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 = ({ const [isGeocoding, setIsGeocoding] = useState(false); const [showToast, setShowToast] = useState(false); const [showInstitutionForm, setShowInstitutionForm] = useState(false); - const [availableCourses, setAvailableCourses] = useState([]); + const [eventTypes, setEventTypes] = useState([]); 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 = ({ endTime: "", type: "", status: EventStatus.PLANNING, + locationName: "", // New field: Nome do Local address: { street: "", number: "", @@ -91,9 +81,10 @@ export const EventForm: React.FC = ({ 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 = ({ ? "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 = ({ 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([]); + 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); + } + setLoadingFots(false); } - } - }, [formData.institutionId, getActiveCoursesByInstitutionId]); + }; + 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 = ({ }; 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 = ({ }, })); } 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 = ({ } }; - // 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 = ({ } }; - const handleSubmit = () => { - // Validate institution selection - if (!formData.institutionId) { - alert("Por favor, selecione uma instituição antes de continuar."); - return; + 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; - } + try { + setShowToast(true); - // Show toast - setShowToast(true); - // Call original submit after small delay for visual effect or immediately - setTimeout(() => { - onSubmit(formData); - }, 1000); + // 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 = ({ 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 (
@@ -337,7 +409,7 @@ export const EventForm: React.FC = ({ : "Preencha as informações técnicas do evento."}

- {/* Step indicators - Hidden on mobile, shown on tablet+ */} + {/* Step indicators */}
{["details", "location", "briefing", "files"].map((tab, idx) => (
= ({
- {/* Mobile Tabs - Horizontal */} + {/* Mobile Tabs */}
{[ @@ -380,11 +452,9 @@ export const EventForm: React.FC = ({ : "text-gray-500 border-transparent hover:bg-gray-50" }`} > - + }"> {item.icon} {item.label} @@ -429,14 +499,22 @@ export const EventForm: React.FC = ({ setFormData({ ...formData, name: e.target.value }) @@ -482,23 +560,21 @@ export const EventForm: React.FC = ({ required /> setFormData({ ...formData, endTime: e.target.value }) } - required />
{ 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 = ({ inputMode="numeric" /> - {/* Institution Selection - OBRIGATÓRIO */} -
- + {/* Dynamic FOT Selection */} +
+

Seleção da Turma

- {userInstitutions.length === 0 ? ( -
-
- -
-

- Nenhuma universidade cadastrada + {!user?.empresaId && user?.role !== UserRole.SUPERADMIN ? ( +

+
+
+
+
+

+ 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.

-

- Você precisa cadastrar uma universidade antes de - criar um evento. Trabalhamos exclusivamente com - eventos fotográficos em universidades. -

-
) : ( -
- + <> + {/* 1. Curso */} +
+ + +
- + {/* 2. Instituição */} +
+ + +
- {formData.institutionId && ( -
- - - Universidade selecionada com sucesso - -
- )} -
+ {/* 3. Ano/Turma (Final FOT Selection) */} +
+ + +
+ )}
- - {/* Course Selection - Condicional baseado na instituição */} - {formData.institutionId && ( -
- - - {availableCourses.length === 0 ? ( -
-
- -
-

- Nenhum curso cadastrado -

-

- Entre em contato com a administração para - cadastrar os cursos/turmas disponíveis nesta - universidade. -

-
-
-
- ) : ( - - )} -
- )}
@@ -647,8 +671,17 @@ export const EventForm: React.FC = ({ {activeTab === "location" && (
-
-
-
+
+
); }; diff --git a/frontend/components/EventTable.tsx b/frontend/components/EventTable.tsx index fc216a2..8223f28 100644 --- a/frontend/components/EventTable.tsx +++ b/frontend/components/EventTable.tsx @@ -132,7 +132,7 @@ export const EventTable: React.FC = ({ [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 = ({ > - {(event as any).fotId || "-"} + {event.fot || "-"} @@ -242,22 +242,22 @@ export const EventTable: React.FC = ({ - {(event as any).curso || "-"} + {event.curso || "-"} - {(event as any).instituicao || "-"} + {event.instituicao || "-"} - {(event as any).anoFormatura || "-"} + {event.anoFormatura || "-"} - {(event as any).empresa || "-"} + {event.empresa || "-"} @@ -265,9 +265,8 @@ export const EventTable: React.FC = ({ {getStatusDisplay(event.status)} diff --git a/frontend/contexts/AuthContext.tsx b/frontend/contexts/AuthContext.tsx index 78138aa..39f19f6 100644 --- a/frontend/contexts/AuthContext.tsx +++ b/frontend/contexts/AuthContext.tsx @@ -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); } diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index 14e1d7a..dd6b2ac 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -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(undefined); + export const DataProvider: React.FC<{ children: ReactNode }> = ({ children, }) => { + const { token, user } = useAuth(); // Consume Auth Context const [events, setEvents] = useState(INITIAL_EVENTS); const [institutions, setInstitutions] = useState(INITIAL_INSTITUTIONS); const [courses, setCourses] = useState(INITIAL_COURSES); const [pendingUsers, setPendingUsers] = useState([]); + // 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) => { - setEvents((prev) => [event, ...prev]); + 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) => { diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index 2b8304c..cecfc9d 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -368,29 +368,27 @@ export const Dashboard: React.FC = ({ {(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && ( -
- - -
- )} +
+ + +
+ )}
{/* Advanced Filters */} @@ -481,9 +479,8 @@ export const Dashboard: React.FC = ({
{selectedEvent.status}
@@ -495,23 +492,23 @@ export const Dashboard: React.FC = ({
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && ( - <> - - - - )} + <> + + + + )} {user.role === UserRole.EVENT_OWNER && selectedEvent.status !== EventStatus.ARCHIVED && ( +
+
+

+ + Equipe ({selectedEvent.photographerIds.length}) +

+ {(user.role === UserRole.BUSINESS_OWNER || + user.role === UserRole.SUPERADMIN) && ( + + )} +
+ + {selectedEvent.photographerIds.length > 0 ? ( +
+ {selectedEvent.photographerIds.map((id) => { + const photographer = MOCK_PHOTOGRAPHERS.find( + (p) => p.id === id + ); + return ( +
+
+ + {photographer?.name || id} + +
+ ); + })} +
+ ) : ( +

+ Nenhum profissional atribuído +

)}
- - {selectedEvent.photographerIds.length > 0 ? ( -
- {selectedEvent.photographerIds.map((id) => { - const photographer = MOCK_PHOTOGRAPHERS.find( - (p) => p.id === id - ); - return ( -
-
- - {photographer?.name || id} - -
- ); - })} -
- ) : ( -

- Nenhum profissional atribuído -

- )} -
- )} + )} @@ -1034,13 +1030,12 @@ export const Dashboard: React.FC = ({ 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]" - : "bg-gray-100 text-gray-400 cursor-not-allowed" - }`} + ? "bg-brand-gold text-white hover:bg-[#a5bd2e]" + : "bg-gray-100 text-gray-400 cursor-not-allowed" + }`} > {isAssigned ? "Remover" : "Adicionar"} @@ -1055,21 +1050,21 @@ export const Dashboard: React.FC = ({ p.availability[selectedEvent.date] ?? false; return isAvailable || isAssigned; }).length === 0 && ( - - -
- -

- Nenhum profissional disponível para esta data -

-

- Tente selecionar outra data ou entre em contato - com a equipe -

-
- - - )} + + +
+ +

+ Nenhum profissional disponível para esta data +

+

+ Tente selecionar outra data ou entre em contato + com a equipe +

+
+ + + )} diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 62427dc..dca49d5 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -177,9 +177,17 @@ export async function getAvailableCourses(): Promise> { +/** + * Busca a listagem de Cadastro FOT + */ +export async function getCadastroFot(token: string, empresaId?: string): Promise> { 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> => { + 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 diff --git a/frontend/types.ts b/frontend/types.ts index 558a479..9f12d3d 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -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 }