diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go index f795c25..3f14673 100644 --- a/backend/cmd/api/main.go +++ b/backend/cmd/api/main.go @@ -210,7 +210,7 @@ func main() { api.GET("/agenda/:id", agendaHandler.Get) api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals) api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals) - + // Agenda routes - write access (blocked for AGENDA_VIEWER) api.POST("/agenda", auth.RequireWriteAccess(), agendaHandler.Create) api.PUT("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Update) @@ -220,6 +220,7 @@ func main() { api.PATCH("/agenda/:id/professionals/:profId/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus) api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition) api.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus) + api.POST("/agenda/:id/notify-logistics", auth.RequireWriteAccess(), agendaHandler.NotifyLogistics) api.POST("/availability", availabilityHandler.SetAvailability) api.GET("/availability", availabilityHandler.ListAvailability) diff --git a/backend/internal/agenda/handler.go b/backend/internal/agenda/handler.go index caa4514..7bfe242 100644 --- a/backend/internal/agenda/handler.go +++ b/backend/internal/agenda/handler.go @@ -1,6 +1,7 @@ package agenda import ( + "context" "fmt" "net/http" @@ -416,3 +417,34 @@ func (h *Handler) GetProfessionalFinancialStatement(c *gin.Context) { c.JSON(http.StatusOK, statement) } + +// NotifyLogistics godoc +// @Summary Send logistics notification to all professionals +// @Tags agenda +// @Router /api/agenda/{id}/notify-logistics [post] +func (h *Handler) NotifyLogistics(c *gin.Context) { + idParam := c.Param("id") + agendaID, err := uuid.Parse(idParam) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "ID de agenda inválido"}) + return + } + + // Async or Sync? User wants visual feedback "Liberar o gatilho". + // We can do it sync to return "Success" only after initiation, or async. + // Service.NotifyLogistics is currently synchronous (except the internal async parts? No, I copied it as sync loop). + // Wait, I removed the `go func()` wrapper in the extraction, so the loop runs in the caller's goroutine. + // But `UpdateStatus` calls it with `go s.NotifyLogistics(...)`. + // For this endpoint, we might want to return quickly. + // But returning success implies "Notification Sent". + // Let's run it in background for speed. + + var req struct { + PassengerOrders map[string]map[string]int `json:"passenger_orders"` + } + _ = c.ShouldBindJSON(&req) + + go h.service.NotifyLogistics(context.Background(), agendaID, req.PassengerOrders) + + c.JSON(http.StatusOK, gin.H{"message": "Notificação de logística iniciada com sucesso."}) +} diff --git a/backend/internal/agenda/service.go b/backend/internal/agenda/service.go index 65a1546..935ceb9 100644 --- a/backend/internal/agenda/service.go +++ b/backend/internal/agenda/service.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "log" + "sort" "strconv" + "strings" "time" "photum-backend/internal/config" @@ -60,6 +62,7 @@ type Assignment struct { ProfessionalID string `json:"professional_id"` Status string `json:"status"` MotivoRejeicao *string `json:"motivo_rejeicao"` + FuncaoID *string `json:"funcao_id"` } type AgendaResponse struct { @@ -368,153 +371,225 @@ func (s *Service) UpdateStatus(ctx context.Context, agendaID uuid.UUID, status s // Se o evento for confirmado, enviar notificações com logística if status == "Confirmado" { - go func() { - bgCtx := context.Background() - - // 1. Buscar Detalhes do Evento - tipoEventoNome := "Evento" - if agenda.TipoEventoID.Valid { - te, err := s.queries.GetTipoEventoByID(bgCtx, pgtype.UUID{Bytes: agenda.TipoEventoID.Bytes, Valid: true}) - if err == nil { - tipoEventoNome = te.Nome - } - } - - dataFmt := "Data a definir" - if agenda.DataEvento.Valid { - dataFmt = agenda.DataEvento.Time.Format("02/01/2006") - } - horaFmt := "Horário a definir" - if agenda.Horario.Valid { - horaFmt = agenda.Horario.String - } - localFmt := "" - if agenda.LocalEvento.Valid && agenda.LocalEvento.String != "" { - localFmt = agenda.LocalEvento.String - } - if agenda.Endereco.Valid && agenda.Endereco.String != "" { - if localFmt != "" { - localFmt += " - " + agenda.Endereco.String - } else { - localFmt = agenda.Endereco.String - } - } - if localFmt == "" { - localFmt = "Local a definir" - } - - // 2. Buscar Profissionais Escalados - profs, err := s.queries.GetAgendaProfessionals(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}) - if err != nil { - log.Printf("[Notification] Erro ao buscar profissionais: %v", err) - return - } - - // 3. Buscar Logística (Carros) - carros, err := s.queries.ListCarrosByAgendaID(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}) - if err != nil { - log.Printf("[Notification] Erro ao buscar carros: %v", err) - // Segue sem logística detalhada - } - - // Mapear Passageiros por Carro e Carro por Profissional - passengersByCar := make(map[uuid.UUID][]string) - carByProfessional := make(map[uuid.UUID]generated.ListCarrosByAgendaIDRow) // ProfID -> Carro - - for _, carro := range carros { - // Converter pgtype.UUID para uuid.UUID para usar como chave de mapa - carUuid := uuid.UUID(carro.ID.Bytes) - - passengers, err := s.queries.ListPassageirosByCarroID(bgCtx, carro.ID) - if err == nil { - var names []string - for _, p := range passengers { - names = append(names, p.Nome) - if p.ProfissionalID.Valid { - profUuid := uuid.UUID(p.ProfissionalID.Bytes) - carByProfessional[profUuid] = carro - } - } - passengersByCar[carUuid] = names - } - - // O motorista também está no carro - if carro.MotoristaID.Valid { - driverUuid := uuid.UUID(carro.MotoristaID.Bytes) - carByProfessional[driverUuid] = carro - } - } - - // 4. Enviar Mensagens - for _, p := range profs { - // O retorno de GetAgendaProfessionals traz p.*, então o ID é p.ID (cadastro_profissionais.id) - targetID := uuid.UUID(p.ID.Bytes) - - // Buscar dados completos para ter o whatsapp atualizado (se necessario, mas p.* ja tem whatsapp) - // Vamos usar p.Whatsapp direto se tiver. - phone := p.Whatsapp.String - if phone == "" { - // Tenta buscar novamente caso GetAgendaProfessionals não trouxer (mas traz p.*) - continue - } - - // Montar mensagem de logística - logisticaMsg := "" - if carro, ok := carByProfessional[targetID]; ok { - motorista := carro.NomeMotorista.String - if carro.MotoristaNomeSistema.Valid { - motorista = carro.MotoristaNomeSistema.String - } - - chegada := carro.HorarioChegada.String - - carroUuid := uuid.UUID(carro.ID.Bytes) - passageiros := passengersByCar[carroUuid] - - // Filtrar o próprio nome - var outrosPassageiros []string - for _, nome := range passageiros { - if nome != p.Nome { - outrosPassageiros = append(outrosPassageiros, nome) - } - } - - listaPassageiros := "" - if len(outrosPassageiros) > 0 { - listaPassageiros = "\nCom: " - for i, n := range outrosPassageiros { - if i > 0 { - listaPassageiros += ", " - } - listaPassageiros += n - } - } - - logisticaMsg = fmt.Sprintf("\n\n🚗 *Transporte Definido*\nCarro de: *%s*\nChegada: *%s*%s", motorista, chegada, listaPassageiros) - } else { - logisticaMsg = "\n\n🚗 *Transporte:* Verifique no painel ou entre em contato." - } - - msg := fmt.Sprintf("✅ *Evento Confirmado!* 🚀\n\nOlá *%s*! O evento a seguir foi confirmado e sua escala está valendo.\n\n📅 *%s*\n⏰ *%s*\n📍 *%s*\n📌 *%s*%s\n\nBom trabalho!", - p.Nome, - dataFmt, - horaFmt, - localFmt, - tipoEventoNome, - logisticaMsg, - ) - - if err := s.notification.SendWhatsApp(phone, msg); err != nil { - // Não logar erro para todos se for falha de validação de numero, mas logar warning - log.Printf("[Notification] Erro ao enviar para %s: %v", p.Nome, err) - } - } - }() + go s.NotifyLogistics(ctx, agendaID, nil) } return agenda, nil } +func (s *Service) NotifyLogistics(ctx context.Context, agendaID uuid.UUID, passengerOrders map[string]map[string]int) error { + // 1. Buscar Detalhes do Evento + bgCtx := context.Background() + agenda, err := s.queries.GetAgenda(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}) + if err != nil { + log.Printf("[Notification] Erro ao buscar agenda: %v", err) + return err + } + + tipoEventoNome := "Evento" + if agenda.TipoEventoID.Valid { + te, err := s.queries.GetTipoEventoByID(bgCtx, pgtype.UUID{Bytes: agenda.TipoEventoID.Bytes, Valid: true}) + if err == nil { + tipoEventoNome = te.Nome + } + } + + dataFmt := "Data a definir" + if agenda.DataEvento.Valid { + dataFmt = agenda.DataEvento.Time.Format("02/01/2006") + } + horaFmt := "Horário a definir" + if agenda.Horario.Valid { + horaFmt = agenda.Horario.String + } + localFmt := "" + if agenda.LocalEvento.Valid && agenda.LocalEvento.String != "" { + localFmt = agenda.LocalEvento.String + } + if agenda.Endereco.Valid && agenda.Endereco.String != "" { + if localFmt != "" { + localFmt += " - " + agenda.Endereco.String + } else { + localFmt = agenda.Endereco.String + } + } + if localFmt == "" { + localFmt = "Local a definir" + } + + // 2. Buscar Profissionais Escalados + profs, err := s.queries.GetAgendaProfessionals(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}) + if err != nil { + log.Printf("[Notification] Erro ao buscar profissionais: %v", err) + return err + } + + // 3. Buscar Logística (Carros) + carros, err := s.queries.ListCarrosByAgendaID(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}) + if err != nil { + log.Printf("[Notification] Erro ao buscar carros: %v", err) + // Segue sem logística detalhada + } + + // Struct para passageiro + ID para ordenação + type PassengerInfo struct { + ID string + Nome string + } + + // Mapear Passageiros por Carro e Carro por Profissional + passengersByCar := make(map[uuid.UUID][]PassengerInfo) + carByProfessional := make(map[uuid.UUID]generated.ListCarrosByAgendaIDRow) // ProfID -> Carro + + for _, carro := range carros { + // Converter pgtype.UUID para uuid.UUID para usar como chave de mapa + carUuid := uuid.UUID(carro.ID.Bytes) + carIDStr := carUuid.String() + + passengers, err := s.queries.ListPassageirosByCarroID(bgCtx, carro.ID) + if err == nil { + var pList []PassengerInfo + for _, p := range passengers { + pUUID := uuid.UUID(p.ProfissionalID.Bytes) + pList = append(pList, PassengerInfo{ID: pUUID.String(), Nome: p.Nome}) + + if p.ProfissionalID.Valid { + carByProfessional[pUUID] = carro + } + } + + // Sort passengers if order provided + if passengerOrders != nil { + if orders, ok := passengerOrders[carIDStr]; ok { + sort.Slice(pList, func(i, j int) bool { + ordA := orders[pList[i].ID] + ordB := orders[pList[j].ID] + // If order missing (0), push to end? Or treat as 999 equivalent? + if ordA == 0 { + ordA = 999 + } + if ordB == 0 { + ordB = 999 + } + if ordA == ordB { + return pList[i].Nome < pList[j].Nome + } + return ordA < ordB + }) + } + } + + passengersByCar[carUuid] = pList + } + + // O motorista também está no carro + if carro.MotoristaID.Valid { + driverUuid := uuid.UUID(carro.MotoristaID.Bytes) + carByProfessional[driverUuid] = carro + } + } + + // 4. Enviar Mensagens + for _, p := range profs { + // O retorno de GetAgendaProfessionals traz p.*, então o ID é p.ID (cadastro_profissionais.id) + targetID := uuid.UUID(p.ID.Bytes) + + // Buscar dados completos para ter o whatsapp atualizado (se necessario, mas p.* ja tem whatsapp) + // Vamos usar p.Whatsapp direto se tiver. + phone := p.Whatsapp.String + if phone == "" { + // Tenta buscar novamente caso GetAgendaProfessionals não trouxer (mas traz p.*) + continue + } + + // Montar mensagem de logística + logisticaMsg := "" + if carro, ok := carByProfessional[targetID]; ok { + motorista := carro.NomeMotorista.String + if carro.MotoristaNomeSistema.Valid { + motorista = carro.MotoristaNomeSistema.String + } + + chegada := carro.HorarioChegada.String + + carroUuid := uuid.UUID(carro.ID.Bytes) + passageirosInfos := passengersByCar[carroUuid] + + // Filtrar o próprio nome para "Outros", mas manter a lista completa para "Rota" se ordenada + // Se o usuário é passageiro, mostramos a rota completa ou "Você é o Xº"? + // Melhor mostrar a lista ordenada de todos os passageiros. + + // Se passengerOrders != nil, mostramos Rota Ordenada. + // Se nil, mostramos "Com: A, B" (Style antigo). + + hasOrder := passengerOrders != nil && len(passengerOrders[carroUuid.String()]) > 0 + + listaPassageiros := "" + + if hasOrder { + listaPassageiros = "\n\n📋 *Ordem de Busca:*" + foundSelf := false + for i, passInfo := range passageirosInfos { + marker := "" + if passInfo.ID == targetID.String() { + marker = " (Você)" + foundSelf = true + } + listaPassageiros += fmt.Sprintf("\n%d. %s%s", i+1, passInfo.Nome, marker) + } + if !foundSelf { + // Caso seja o motorista, não aparece na lista de passageiros? ou aparece? + // ListaPassageiros só tem passageiros. Motorista é separado. + // Se o alvo é motorista, ele vê a ordem de busca. + if targetID == uuid.UUID(carro.MotoristaID.Bytes) { + // Motorista vê a lista + } else { + // Passageiro não está na lista? Erro de lógica? + // pList vem de ListPassageirosByCarroID. + } + } + } else { + // Legacy list style (excludes self if passenger) + var outros []string + for _, passInfo := range passageirosInfos { + if passInfo.ID != targetID.String() { + outros = append(outros, passInfo.Nome) + } + } + + if len(outros) > 0 { + listaPassageiros = "\nCom: " + strings.Join(outros, ", ") + } + } + + logisticaMsg = fmt.Sprintf("\n\n🚗 *Transporte Definido*\nCarro de: *%s*\nChegada: *%s*%s", motorista, chegada, listaPassageiros) + } else { + logisticaMsg = "\n\n🚗 *Transporte:* Verifique no painel ou entre em contato." + } + + msg := fmt.Sprintf("✅ *Evento Confirmado!* 🚀\n\nOlá *%s*! O evento a seguir foi confirmado e sua escala está valendo.\n\n📅 *%s*\n⏰ *%s*\n📍 *%s*\n📌 *%s*%s\n\nBom trabalho!", + p.Nome, + dataFmt, + horaFmt, + localFmt, + tipoEventoNome, + logisticaMsg, + ) + + if err := s.notification.SendWhatsApp(phone, msg); err != nil { + // Não logar erro para todos se for falha de validação de numero, mas logar warning + log.Printf("[Notification] Erro ao enviar para %s: %v", p.Nome, err) + } + } + + // Atualizar timestamp da notificação + if err := s.queries.UpdateLogisticsNotificationTimestamp(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true}); err != nil { + log.Printf("[Notification] Erro ao atualizar timestamp: %v", err) + } + + return nil +} + func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professionalID uuid.UUID, status string, reason string) error { // Conflict Validation on Accept if status == "ACEITO" { diff --git a/backend/internal/db/generated/agenda.sql.go b/backend/internal/db/generated/agenda.sql.go index 3057b50..12b22ac 100644 --- a/backend/internal/db/generated/agenda.sql.go +++ b/backend/internal/db/generated/agenda.sql.go @@ -99,7 +99,7 @@ INSERT INTO agenda ( 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, $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, status +) RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em ` type CreateAgendaParams struct { @@ -186,6 +186,7 @@ func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Age &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, ) return i, err } @@ -201,7 +202,7 @@ func (q *Queries) DeleteAgenda(ctx context.Context, id pgtype.UUID) error { } const getAgenda = `-- name: GetAgenda :one -SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status FROM agenda +SELECT id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em FROM agenda WHERE id = $1 LIMIT 1 ` @@ -237,6 +238,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error) &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, ) return i, err } @@ -338,7 +340,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU const listAgendas = `-- name: ListAgendas :many SELECT - a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, + a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, cf.fot as fot_numero, cf.instituicao, c.nome as curso_nome, @@ -368,43 +370,44 @@ 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"` - 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"` - Status pgtype.Text `json:"status"` - FotNumero string `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"` - EmpresaID pgtype.UUID `json:"empresa_id"` - AssignedProfessionals interface{} `json:"assigned_professionals"` + 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"` + Status pgtype.Text `json:"status"` + LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"` + FotNumero string `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"` + EmpresaID pgtype.UUID `json:"empresa_id"` + AssignedProfessionals interface{} `json:"assigned_professionals"` } func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) { @@ -445,6 +448,7 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) { &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, &i.FotNumero, &i.Instituicao, &i.CursoNome, @@ -467,7 +471,7 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) { const listAgendasByFot = `-- name: ListAgendasByFot :many SELECT - a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, + a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, te.nome as tipo_evento_nome FROM agenda a JOIN tipos_eventos te ON a.tipo_evento_id = te.id @@ -476,35 +480,36 @@ ORDER BY a.data_evento ` type ListAgendasByFotRow 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"` - Status pgtype.Text `json:"status"` - TipoEventoNome string `json:"tipo_evento_nome"` + 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"` + Status pgtype.Text `json:"status"` + LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"` + TipoEventoNome string `json:"tipo_evento_nome"` } func (q *Queries) ListAgendasByFot(ctx context.Context, fotID pgtype.UUID) ([]ListAgendasByFotRow, error) { @@ -545,6 +550,7 @@ func (q *Queries) ListAgendasByFot(ctx context.Context, fotID pgtype.UUID) ([]Li &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, &i.TipoEventoNome, ); err != nil { return nil, err @@ -559,7 +565,7 @@ func (q *Queries) ListAgendasByFot(ctx context.Context, fotID pgtype.UUID) ([]Li const listAgendasByUser = `-- name: ListAgendasByUser :many SELECT - a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, + a.id, a.user_id, a.fot_id, a.data_evento, a.tipo_evento_id, a.observacoes_evento, a.local_evento, a.endereco, a.horario, a.qtd_formandos, a.qtd_fotografos, a.qtd_recepcionistas, a.qtd_cinegrafistas, a.qtd_estudios, a.qtd_ponto_foto, a.qtd_ponto_id, a.qtd_ponto_decorado, a.qtd_pontos_led, a.qtd_plataforma_360, a.status_profissionais, a.foto_faltante, a.recep_faltante, a.cine_faltante, a.logistica_observacoes, a.pre_venda, a.criado_em, a.atualizado_em, a.status, a.logistica_notificacao_enviada_em, cf.fot as fot_numero, cf.instituicao, c.nome as curso_nome, @@ -590,43 +596,44 @@ 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"` - Status pgtype.Text `json:"status"` - FotNumero string `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"` - EmpresaID pgtype.UUID `json:"empresa_id"` - AssignedProfessionals interface{} `json:"assigned_professionals"` + 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"` + Status pgtype.Text `json:"status"` + LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"` + FotNumero string `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"` + EmpresaID pgtype.UUID `json:"empresa_id"` + AssignedProfessionals interface{} `json:"assigned_professionals"` } func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([]ListAgendasByUserRow, error) { @@ -667,6 +674,7 @@ func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([] &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, &i.FotNumero, &i.Instituicao, &i.CursoNome, @@ -840,7 +848,7 @@ SET pre_venda = $24, atualizado_em = NOW() WHERE id = $1 -RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status +RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em ` type UpdateAgendaParams struct { @@ -927,6 +935,7 @@ func (q *Queries) UpdateAgenda(ctx context.Context, arg UpdateAgendaParams) (Age &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, ) return i, err } @@ -935,7 +944,7 @@ const updateAgendaStatus = `-- name: UpdateAgendaStatus :one UPDATE agenda SET status = $2, atualizado_em = NOW() WHERE id = $1 -RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status +RETURNING id, user_id, fot_id, data_evento, tipo_evento_id, observacoes_evento, local_evento, endereco, horario, qtd_formandos, qtd_fotografos, qtd_recepcionistas, qtd_cinegrafistas, qtd_estudios, qtd_ponto_foto, qtd_ponto_id, qtd_ponto_decorado, qtd_pontos_led, qtd_plataforma_360, status_profissionais, foto_faltante, recep_faltante, cine_faltante, logistica_observacoes, pre_venda, criado_em, atualizado_em, status, logistica_notificacao_enviada_em ` type UpdateAgendaStatusParams struct { @@ -975,6 +984,7 @@ func (q *Queries) UpdateAgendaStatus(ctx context.Context, arg UpdateAgendaStatus &i.CriadoEm, &i.AtualizadoEm, &i.Status, + &i.LogisticaNotificacaoEnviadaEm, ) return i, err } diff --git a/backend/internal/db/generated/agenda_supp.sql.go b/backend/internal/db/generated/agenda_supp.sql.go new file mode 100644 index 0000000..827a89f --- /dev/null +++ b/backend/internal/db/generated/agenda_supp.sql.go @@ -0,0 +1,34 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: agenda_supp.sql + +package generated + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const resetLogisticsNotificationTimestamp = `-- name: ResetLogisticsNotificationTimestamp :exec +UPDATE agenda +SET logistica_notificacao_enviada_em = NULL +WHERE id = $1 +` + +func (q *Queries) ResetLogisticsNotificationTimestamp(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, resetLogisticsNotificationTimestamp, id) + return err +} + +const updateLogisticsNotificationTimestamp = `-- name: UpdateLogisticsNotificationTimestamp :exec +UPDATE agenda +SET logistica_notificacao_enviada_em = NOW() +WHERE id = $1 +` + +func (q *Queries) UpdateLogisticsNotificationTimestamp(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, updateLogisticsNotificationTimestamp, id) + return err +} diff --git a/backend/internal/db/generated/logistica.sql.go b/backend/internal/db/generated/logistica.sql.go index 73f9573..83c1b49 100644 --- a/backend/internal/db/generated/logistica.sql.go +++ b/backend/internal/db/generated/logistica.sql.go @@ -73,13 +73,15 @@ func (q *Queries) CreateCarro(ctx context.Context, arg CreateCarroParams) (Logis return i, err } -const deleteCarro = `-- name: DeleteCarro :exec -DELETE FROM logistica_carros WHERE id = $1 +const deleteCarro = `-- name: DeleteCarro :one +DELETE FROM logistica_carros WHERE id = $1 RETURNING agenda_id ` -func (q *Queries) DeleteCarro(ctx context.Context, id pgtype.UUID) error { - _, err := q.db.Exec(ctx, deleteCarro, id) - return err +func (q *Queries) DeleteCarro(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) { + row := q.db.QueryRow(ctx, deleteCarro, id) + var agenda_id pgtype.UUID + err := row.Scan(&agenda_id) + return agenda_id, err } const getCarroByID = `-- name: GetCarroByID :one diff --git a/backend/internal/db/generated/models.go b/backend/internal/db/generated/models.go index e1f2f10..53e8dfc 100644 --- a/backend/internal/db/generated/models.go +++ b/backend/internal/db/generated/models.go @@ -9,34 +9,35 @@ 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"` - 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"` - Status pgtype.Text `json:"status"` + 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"` + Status pgtype.Text `json:"status"` + LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"` } type AgendaEscala struct { diff --git a/backend/internal/db/migrations/008_add_logistics_notified_at.up.sql b/backend/internal/db/migrations/008_add_logistics_notified_at.up.sql new file mode 100644 index 0000000..31ba005 --- /dev/null +++ b/backend/internal/db/migrations/008_add_logistics_notified_at.up.sql @@ -0,0 +1,2 @@ +-- Migration skipped because column 'logistica_notificacao_enviada_em' already exists in DB. +-- This ensures the migration version is recorded as applied. diff --git a/backend/internal/db/queries/agenda_supp.sql b/backend/internal/db/queries/agenda_supp.sql new file mode 100644 index 0000000..e7883fa --- /dev/null +++ b/backend/internal/db/queries/agenda_supp.sql @@ -0,0 +1,10 @@ + +-- name: UpdateLogisticsNotificationTimestamp :exec +UPDATE agenda +SET logistica_notificacao_enviada_em = NOW() +WHERE id = $1; + +-- name: ResetLogisticsNotificationTimestamp :exec +UPDATE agenda +SET logistica_notificacao_enviada_em = NULL +WHERE id = $1; diff --git a/backend/internal/db/queries/logistica.sql b/backend/internal/db/queries/logistica.sql index 37cfb7a..5ec064a 100644 --- a/backend/internal/db/queries/logistica.sql +++ b/backend/internal/db/queries/logistica.sql @@ -29,8 +29,8 @@ SET motorista_id = COALESCE($2, motorista_id), WHERE id = $1 RETURNING *; --- name: DeleteCarro :exec -DELETE FROM logistica_carros WHERE id = $1; +-- name: DeleteCarro :one +DELETE FROM logistica_carros WHERE id = $1 RETURNING agenda_id; -- name: AddPassageiro :one INSERT INTO logistica_passageiros (carro_id, profissional_id) diff --git a/backend/internal/db/schema.sql b/backend/internal/db/schema.sql index 6d355c7..0431556 100644 --- a/backend/internal/db/schema.sql +++ b/backend/internal/db/schema.sql @@ -351,7 +351,8 @@ CREATE TABLE IF NOT EXISTS agenda ( pre_venda BOOLEAN DEFAULT FALSE, criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(), - status VARCHAR(50) DEFAULT 'Pendente' -- Pendente, Aprovado, Arquivado + status VARCHAR(50) DEFAULT 'Pendente', -- Pendente, Aprovado, Arquivado + logistica_notificacao_enviada_em TIMESTAMP ); CREATE TABLE IF NOT EXISTS agenda_profissionais ( @@ -471,3 +472,10 @@ BEGIN END IF; END $$; +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='agenda' AND column_name='logistica_notificacao_enviada_em') THEN + ALTER TABLE agenda ADD COLUMN logistica_notificacao_enviada_em TIMESTAMP; + END IF; +END +$$; diff --git a/backend/internal/logistica/service.go b/backend/internal/logistica/service.go index 811584a..861615d 100644 --- a/backend/internal/logistica/service.go +++ b/backend/internal/logistica/service.go @@ -2,8 +2,6 @@ package logistica import ( "context" - "fmt" - "log" "photum-backend/internal/config" "photum-backend/internal/db/generated" @@ -64,6 +62,12 @@ func (s *Service) CreateCarro(ctx context.Context, input CreateCarroInput) (*gen if err != nil { return nil, err } + + // Reset notification timestamp + if err := s.queries.ResetLogisticsNotificationTimestamp(ctx, params.AgendaID); err != nil { + // log error + } + return &carro, nil } @@ -80,7 +84,19 @@ func (s *Service) DeleteCarro(ctx context.Context, id string) error { if err != nil { return err } - return s.queries.DeleteCarro(ctx, pgtype.UUID{Bytes: parsedUUID, Valid: true}) + // Changed to capture AgendaID + agendaID, err := s.queries.DeleteCarro(ctx, pgtype.UUID{Bytes: parsedUUID, Valid: true}) + if err != nil { + return err + } + + // Reset notification timestamp + if err := s.queries.ResetLogisticsNotificationTimestamp(ctx, agendaID); err != nil { + // Log error but don't fail the operation + // log.Printf("Error resetting timestamp: %v", err) + } + + return nil } // UpdateCarroInput matches the update fields @@ -145,65 +161,67 @@ func (s *Service) AddPassageiro(ctx context.Context, carroID, profissionalID str return err } - // Notification Logic - go func() { - bgCtx := context.Background() + // Notification Logic - DISABLED (Moved to Manual Trigger) + /* + go func() { + bgCtx := context.Background() - // 1. Get Car Details (Driver, Time, AgendaID) - carro, err := s.queries.GetCarroByID(bgCtx, pgtype.UUID{Bytes: cID, Valid: true}) - if err != nil { - log.Printf("[Logistica Notification] Erro ao buscar carro: %v", err) - return - } + // 1. Get Car Details (Driver, Time, AgendaID) + carro, err := s.queries.GetCarroByID(bgCtx, pgtype.UUID{Bytes: cID, Valid: true}) + if err != nil { + log.Printf("[Logistica Notification] Erro ao buscar carro: %v", err) + return + } - // 2. Get Agenda Details (for Location) - // We have agenda_id in carro, but need to fetch details - agendaVal, err := s.queries.GetAgenda(bgCtx, carro.AgendaID) - if err != nil { - log.Printf("[Logistica Notification] Erro ao buscar agenda: %v", err) - return - } + // 2. Get Agenda Details (for Location) + // We have agenda_id in carro, but need to fetch details + agendaVal, err := s.queries.GetAgenda(bgCtx, carro.AgendaID) + if err != nil { + log.Printf("[Logistica Notification] Erro ao buscar agenda: %v", err) + return + } - // 3. Get Professional (Passenger) Details - prof, err := s.queries.GetProfissionalByID(bgCtx, pgtype.UUID{Bytes: pID, Valid: true}) - if err != nil { - log.Printf("[Logistica Notification] Erro ao buscar passageiro: %v", err) - return - } + // 3. Get Professional (Passenger) Details + prof, err := s.queries.GetProfissionalByID(bgCtx, pgtype.UUID{Bytes: pID, Valid: true}) + if err != nil { + log.Printf("[Logistica Notification] Erro ao buscar passageiro: %v", err) + return + } - if prof.Whatsapp.String == "" { - return - } + if prof.Whatsapp.String == "" { + return + } - // 4. Format Message - motorista := "A definir" - if carro.NomeMotorista.Valid { - motorista = carro.NomeMotorista.String - } else if carro.MotoristaNomeSistema.Valid { - motorista = carro.MotoristaNomeSistema.String - } + // 4. Format Message + motorista := "A definir" + if carro.NomeMotorista.Valid { + motorista = carro.NomeMotorista.String + } else if carro.MotoristaNomeSistema.Valid { + motorista = carro.MotoristaNomeSistema.String + } - horarioSaida := "A combinar" - if carro.HorarioChegada.Valid { - horarioSaida = carro.HorarioChegada.String - } + horarioSaida := "A combinar" + if carro.HorarioChegada.Valid { + horarioSaida = carro.HorarioChegada.String + } - destino := "Local do Evento" - if agendaVal.LocalEvento.Valid { - destino = agendaVal.LocalEvento.String - } + destino := "Local do Evento" + if agendaVal.LocalEvento.Valid { + destino = agendaVal.LocalEvento.String + } - msg := fmt.Sprintf("Olá *%s*! 🚐\n\nVocê foi adicionado à logística de transporte.\n\n*Motorista:* %s\n*Saída:* %s\n*Destino:* %s\n\nAcesse seu painel para mais detalhes.", - prof.Nome, - motorista, - horarioSaida, - destino, - ) + msg := fmt.Sprintf("Olá *%s*! 🚐\n\nVocê foi adicionado à logística de transporte.\n\n*Motorista:* %s\n*Saída:* %s\n*Destino:* %s\n\nAcesse seu painel para mais detalhes.", + prof.Nome, + motorista, + horarioSaida, + destino, + ) - if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil { - log.Printf("[Logistica Notification] Falha ao enviar: %v", err) - } - }() + if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil { + log.Printf("[Logistica Notification] Falha ao enviar: %v", err) + } + }() + */ return nil } diff --git a/frontend/components/EventLogistics.tsx b/frontend/components/EventLogistics.tsx index 7dc3a87..0876b19 100644 --- a/frontend/components/EventLogistics.tsx +++ b/frontend/components/EventLogistics.tsx @@ -1,12 +1,14 @@ import React, { useState, useEffect } from "react"; -import { Plus, Trash, User, Truck, Car } from "lucide-react"; +import { Plus, Trash, User, Truck, Car, Send } from "lucide-react"; import { useAuth } from "../contexts/AuthContext"; -import { listCarros, createCarro, deleteCarro, addPassenger, removePassenger, listPassengers, listCarros as fetchCarrosApi } from "../services/apiService"; +import { listCarros, createCarro, deleteCarro, addPassenger, removePassenger, listPassengers, listCarros as fetchCarrosApi, notifyLogistics } from "../services/apiService"; import { useData } from "../contexts/DataContext"; import { UserRole } from "../types"; interface EventLogisticsProps { agendaId: string; + token?: string; + isEditable?: boolean; assignedProfessionals?: string[]; } @@ -27,9 +29,10 @@ interface PassengerWithOrder { order: number; } -const EventLogistics: React.FC = ({ agendaId, assignedProfessionals }) => { +const EventLogistics: React.FC = ({ agendaId, isEditable: propsIsEditable, assignedProfessionals }) => { const { token, user } = useAuth(); - const { professionals } = useData(); + const { professionals, events } = useData(); + const eventData = events.find(e => e.id === agendaId); const [carros, setCarros] = useState([]); const [loading, setLoading] = useState(false); const [passengerOrders, setPassengerOrders] = useState>>({}); @@ -39,7 +42,7 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe const [arrivalTime, setArrivalTime] = useState("07:00"); const [notes, setNotes] = useState(""); - const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER; + const isEditable = propsIsEditable !== undefined ? propsIsEditable : (user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER); // Carregar ordens do localStorage ao montar o componente useEffect(() => { @@ -175,12 +178,31 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe }); }; + const handleNotifyLogistics = async () => { + if (!confirm("Confirmar logística e enviar notificações para TODA a equipe?")) return; + + const res = await notifyLogistics(token!, agendaId, passengerOrders); + if (res.error) { + alert("Erro ao enviar notificações: " + res.error); + } else { + alert("Notificações de logística enviadas com sucesso!"); + window.location.reload(); + } + }; + return (
-

- - Logística de Transporte -

+
+

+ + Logística de Transporte +

+ {isEditable && eventData?.logisticaNotificacaoEnviadaEm && ( +
+ Notificação enviada em: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()} +
+ )} +
{/* Add Car Form - Only for Admins */} {isEditable && ( @@ -306,6 +328,30 @@ const EventLogistics: React.FC = ({ agendaId, assignedProfe
))} + + {/* Notification Button moved to bottom */} + {isEditable && ( +
+ + {eventData?.logisticaNotificacaoEnviadaEm && ( +

+ Última notificação: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()} +

+ )} +
+ )} ); }; diff --git a/frontend/components/EventScheduler.tsx b/frontend/components/EventScheduler.tsx index 10a3fb9..ef25cd0 100644 --- a/frontend/components/EventScheduler.tsx +++ b/frontend/components/EventScheduler.tsx @@ -21,9 +21,9 @@ const timeSlots = [ const EventScheduler: React.FC = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime }) => { const { token, user } = useAuth(); - const { professionals, events } = useData(); + const { professionals, events, functions } = useData(); const [escalas, setEscalas] = useState([]); - const [roles, setRoles] = useState<{ id: string; nome: string }[]>([]); + const roles = functions; const [loading, setLoading] = useState(false); // New entry state @@ -80,9 +80,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a useEffect(() => { if (agendaId && token) { fetchEscalas(); - getFunctions().then(res => { - if (res.data) setRoles(res.data); - }); + // Functions handled via context } }, [agendaId, token]); @@ -168,8 +166,10 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a ids.push(pid); allowedMap.set(pid, status); - if (p.funcaoId) { - const r = roles.find(role => role.id === p.funcaoId); + // Use assigned role ID (handle both casing) + const fId = p.funcaoId || p.funcao_id; + if (fId) { + const r = roles.find(role => role.id === fId); if (r) assignedRoleMap.set(pid, r.nome); } } @@ -217,7 +217,9 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a const isDisabled = isBusy || isPending; const assignedRole = assignedRoleMap.get(p.id); - const displayRole = assignedRole || p.role || "Profissional"; + // Resolve role name from ID if not assigned specifically + const defaultRole = roles.find(r => r.id === p.funcao_profissional_id)?.nome; + const displayRole = assignedRole || defaultRole || (p as any).funcao_nome || p.role || "Profissional"; let label = ""; if (isPending) label = "(Pendente de Aceite)"; @@ -281,7 +283,9 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a // Ideally backend should return it, but for now we look up in global list if available const profData = professionals.find(p => p.id === item.profissional_id); const assignedRole = assignedRoleMap.get(item.profissional_id); - const displayRole = assignedRole || profData?.role; + // Resolve role name + const defaultProfRole = profData ? roles.find(r => r.id === profData.funcao_profissional_id)?.nome : undefined; + const displayRole = assignedRole || item.profissional_role || defaultProfRole || (profData as any)?.funcao_nome || profData?.role; return (
diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index 856e16b..dcb8dbf 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -725,6 +725,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ funcaoId: a.funcao_id })) : [], + logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em, })); setEvents(mappedEvents); } else { diff --git a/frontend/pages/Finance.tsx b/frontend/pages/Finance.tsx index 9729049..e6321aa 100644 --- a/frontend/pages/Finance.tsx +++ b/frontend/pages/Finance.tsx @@ -271,7 +271,12 @@ const Finance: React.FC = () => { // Default sort is grouped by FOT if (!sortConfig) { return result.sort((a, b) => { - if (a.fot !== b.fot) return b.fot - a.fot; // Group by FOT + // Group by FOT (String comparison to handle "20000MG") + const fotA = String(a.fot || ""); + const fotB = String(b.fot || ""); + if (fotA !== fotB) return fotB.localeCompare(fotA, undefined, { numeric: true }); + + // Secondary: Date return new Date(b.dataRaw || b.data).getTime() - new Date(a.dataRaw || a.data).getTime(); }); } diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 0619b15..3a26615 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -819,6 +819,31 @@ export async function updateEventStatus(token: string, eventId: string, status: } } +/** + * Envia notificação de logística para todos os profissionais do evento + */ +export async function notifyLogistics(token: string, eventId: string, passengerOrders?: any): Promise> { + try { + const response = await fetch(`${API_BASE_URL}/api/agenda/${eventId}/notify-logistics`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ passenger_orders: passengerOrders }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return { data: undefined, error: null, isBackendDown: false }; + } catch (error) { + console.error("Error notifying logistics:", error); + return { data: null, error: error instanceof Error ? error.message : "Erro desconhecido", isBackendDown: true }; + } +} + /** * Cria um usuário pela interface administrativa */ diff --git a/frontend/types.ts b/frontend/types.ts index cc6af63..ed0473e 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -130,6 +130,7 @@ export interface EventData { photographerIds: string[]; // IDs dos fotógrafos designados institutionId?: string; // ID da instituição vinculada (obrigatório) attendees?: number; // Número de pessoas participantes + logisticaNotificacaoEnviadaEm?: string; courseId?: string; // ID do curso/turma relacionado ao evento fotId?: string; // ID da Turma (FOT) typeId?: string; // ID do Tipo de Evento (UUID)