feat: improve logistics notification persistence and finance grouping
This commit is contained in:
parent
c1af6eb8b4
commit
6b9299dd7a
18 changed files with 636 additions and 361 deletions
|
|
@ -210,7 +210,7 @@ func main() {
|
||||||
api.GET("/agenda/:id", agendaHandler.Get)
|
api.GET("/agenda/:id", agendaHandler.Get)
|
||||||
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
|
api.GET("/agenda/:id/professionals", agendaHandler.GetProfessionals)
|
||||||
api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals)
|
api.GET("/agenda/:id/available", agendaHandler.ListAvailableProfessionals)
|
||||||
|
|
||||||
// Agenda routes - write access (blocked for AGENDA_VIEWER)
|
// Agenda routes - write access (blocked for AGENDA_VIEWER)
|
||||||
api.POST("/agenda", auth.RequireWriteAccess(), agendaHandler.Create)
|
api.POST("/agenda", auth.RequireWriteAccess(), agendaHandler.Create)
|
||||||
api.PUT("/agenda/:id", auth.RequireWriteAccess(), agendaHandler.Update)
|
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/status", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentStatus)
|
||||||
api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition)
|
api.PATCH("/agenda/:id/professionals/:profId/position", auth.RequireWriteAccess(), agendaHandler.UpdateAssignmentPosition)
|
||||||
api.PATCH("/agenda/:id/status", auth.RequireWriteAccess(), agendaHandler.UpdateStatus)
|
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.POST("/availability", availabilityHandler.SetAvailability)
|
||||||
api.GET("/availability", availabilityHandler.ListAvailability)
|
api.GET("/availability", availabilityHandler.ListAvailability)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package agenda
|
package agenda
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
|
@ -416,3 +417,34 @@ func (h *Handler) GetProfessionalFinancialStatement(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, statement)
|
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."})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"photum-backend/internal/config"
|
"photum-backend/internal/config"
|
||||||
|
|
@ -60,6 +62,7 @@ type Assignment struct {
|
||||||
ProfessionalID string `json:"professional_id"`
|
ProfessionalID string `json:"professional_id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
||||||
|
FuncaoID *string `json:"funcao_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgendaResponse struct {
|
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
|
// Se o evento for confirmado, enviar notificações com logística
|
||||||
if status == "Confirmado" {
|
if status == "Confirmado" {
|
||||||
go func() {
|
go s.NotifyLogistics(ctx, agendaID, nil)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return agenda, 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 {
|
func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professionalID uuid.UUID, status string, reason string) error {
|
||||||
// Conflict Validation on Accept
|
// Conflict Validation on Accept
|
||||||
if status == "ACEITO" {
|
if status == "ACEITO" {
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ INSERT INTO agenda (
|
||||||
user_id
|
user_id
|
||||||
) VALUES (
|
) 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
|
$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 {
|
type CreateAgendaParams struct {
|
||||||
|
|
@ -186,6 +186,7 @@ func (q *Queries) CreateAgenda(ctx context.Context, arg CreateAgendaParams) (Age
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -201,7 +202,7 @@ func (q *Queries) DeleteAgenda(ctx context.Context, id pgtype.UUID) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAgenda = `-- name: GetAgenda :one
|
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
|
WHERE id = $1 LIMIT 1
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
@ -237,6 +238,7 @@ func (q *Queries) GetAgenda(ctx context.Context, id pgtype.UUID) (Agenda, error)
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -338,7 +340,7 @@ func (q *Queries) GetAgendaProfessionals(ctx context.Context, agendaID pgtype.UU
|
||||||
|
|
||||||
const listAgendas = `-- name: ListAgendas :many
|
const listAgendas = `-- name: ListAgendas :many
|
||||||
SELECT
|
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.fot as fot_numero,
|
||||||
cf.instituicao,
|
cf.instituicao,
|
||||||
c.nome as curso_nome,
|
c.nome as curso_nome,
|
||||||
|
|
@ -368,43 +370,44 @@ ORDER BY a.data_evento
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListAgendasRow struct {
|
type ListAgendasRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
FotID pgtype.UUID `json:"fot_id"`
|
FotID pgtype.UUID `json:"fot_id"`
|
||||||
DataEvento pgtype.Date `json:"data_evento"`
|
DataEvento pgtype.Date `json:"data_evento"`
|
||||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||||
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
||||||
LocalEvento pgtype.Text `json:"local_evento"`
|
LocalEvento pgtype.Text `json:"local_evento"`
|
||||||
Endereco pgtype.Text `json:"endereco"`
|
Endereco pgtype.Text `json:"endereco"`
|
||||||
Horario pgtype.Text `json:"horario"`
|
Horario pgtype.Text `json:"horario"`
|
||||||
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
||||||
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
||||||
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
||||||
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
||||||
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
||||||
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
||||||
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
||||||
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
||||||
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
||||||
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
||||||
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
||||||
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
||||||
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
||||||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
FotNumero string `json:"fot_numero"`
|
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||||
Instituicao pgtype.Text `json:"instituicao"`
|
FotNumero string `json:"fot_numero"`
|
||||||
CursoNome string `json:"curso_nome"`
|
Instituicao pgtype.Text `json:"instituicao"`
|
||||||
EmpresaNome string `json:"empresa_nome"`
|
CursoNome string `json:"curso_nome"`
|
||||||
AnoSemestre string `json:"ano_semestre"`
|
EmpresaNome string `json:"empresa_nome"`
|
||||||
ObservacoesFot pgtype.Text `json:"observacoes_fot"`
|
AnoSemestre string `json:"ano_semestre"`
|
||||||
TipoEventoNome string `json:"tipo_evento_nome"`
|
ObservacoesFot pgtype.Text `json:"observacoes_fot"`
|
||||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
TipoEventoNome string `json:"tipo_evento_nome"`
|
||||||
AssignedProfessionals interface{} `json:"assigned_professionals"`
|
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||||
|
AssignedProfessionals interface{} `json:"assigned_professionals"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) {
|
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.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
&i.FotNumero,
|
&i.FotNumero,
|
||||||
&i.Instituicao,
|
&i.Instituicao,
|
||||||
&i.CursoNome,
|
&i.CursoNome,
|
||||||
|
|
@ -467,7 +471,7 @@ func (q *Queries) ListAgendas(ctx context.Context) ([]ListAgendasRow, error) {
|
||||||
|
|
||||||
const listAgendasByFot = `-- name: ListAgendasByFot :many
|
const listAgendasByFot = `-- name: ListAgendasByFot :many
|
||||||
SELECT
|
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
|
te.nome as tipo_evento_nome
|
||||||
FROM agenda a
|
FROM agenda a
|
||||||
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
JOIN tipos_eventos te ON a.tipo_evento_id = te.id
|
||||||
|
|
@ -476,35 +480,36 @@ ORDER BY a.data_evento
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListAgendasByFotRow struct {
|
type ListAgendasByFotRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
FotID pgtype.UUID `json:"fot_id"`
|
FotID pgtype.UUID `json:"fot_id"`
|
||||||
DataEvento pgtype.Date `json:"data_evento"`
|
DataEvento pgtype.Date `json:"data_evento"`
|
||||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||||
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
||||||
LocalEvento pgtype.Text `json:"local_evento"`
|
LocalEvento pgtype.Text `json:"local_evento"`
|
||||||
Endereco pgtype.Text `json:"endereco"`
|
Endereco pgtype.Text `json:"endereco"`
|
||||||
Horario pgtype.Text `json:"horario"`
|
Horario pgtype.Text `json:"horario"`
|
||||||
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
||||||
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
||||||
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
||||||
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
||||||
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
||||||
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
||||||
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
||||||
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
||||||
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
||||||
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
||||||
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
||||||
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
||||||
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
||||||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
TipoEventoNome string `json:"tipo_evento_nome"`
|
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) {
|
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.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
&i.TipoEventoNome,
|
&i.TipoEventoNome,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -559,7 +565,7 @@ func (q *Queries) ListAgendasByFot(ctx context.Context, fotID pgtype.UUID) ([]Li
|
||||||
|
|
||||||
const listAgendasByUser = `-- name: ListAgendasByUser :many
|
const listAgendasByUser = `-- name: ListAgendasByUser :many
|
||||||
SELECT
|
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.fot as fot_numero,
|
||||||
cf.instituicao,
|
cf.instituicao,
|
||||||
c.nome as curso_nome,
|
c.nome as curso_nome,
|
||||||
|
|
@ -590,43 +596,44 @@ ORDER BY a.data_evento
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListAgendasByUserRow struct {
|
type ListAgendasByUserRow struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
FotID pgtype.UUID `json:"fot_id"`
|
FotID pgtype.UUID `json:"fot_id"`
|
||||||
DataEvento pgtype.Date `json:"data_evento"`
|
DataEvento pgtype.Date `json:"data_evento"`
|
||||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||||
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
||||||
LocalEvento pgtype.Text `json:"local_evento"`
|
LocalEvento pgtype.Text `json:"local_evento"`
|
||||||
Endereco pgtype.Text `json:"endereco"`
|
Endereco pgtype.Text `json:"endereco"`
|
||||||
Horario pgtype.Text `json:"horario"`
|
Horario pgtype.Text `json:"horario"`
|
||||||
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
||||||
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
||||||
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
||||||
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
||||||
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
||||||
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
||||||
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
||||||
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
||||||
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
||||||
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
||||||
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
||||||
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
||||||
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
||||||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
FotNumero string `json:"fot_numero"`
|
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||||
Instituicao pgtype.Text `json:"instituicao"`
|
FotNumero string `json:"fot_numero"`
|
||||||
CursoNome string `json:"curso_nome"`
|
Instituicao pgtype.Text `json:"instituicao"`
|
||||||
EmpresaNome string `json:"empresa_nome"`
|
CursoNome string `json:"curso_nome"`
|
||||||
AnoSemestre string `json:"ano_semestre"`
|
EmpresaNome string `json:"empresa_nome"`
|
||||||
ObservacoesFot pgtype.Text `json:"observacoes_fot"`
|
AnoSemestre string `json:"ano_semestre"`
|
||||||
TipoEventoNome string `json:"tipo_evento_nome"`
|
ObservacoesFot pgtype.Text `json:"observacoes_fot"`
|
||||||
EmpresaID pgtype.UUID `json:"empresa_id"`
|
TipoEventoNome string `json:"tipo_evento_nome"`
|
||||||
AssignedProfessionals interface{} `json:"assigned_professionals"`
|
EmpresaID pgtype.UUID `json:"empresa_id"`
|
||||||
|
AssignedProfessionals interface{} `json:"assigned_professionals"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListAgendasByUser(ctx context.Context, userID pgtype.UUID) ([]ListAgendasByUserRow, error) {
|
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.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
&i.FotNumero,
|
&i.FotNumero,
|
||||||
&i.Instituicao,
|
&i.Instituicao,
|
||||||
&i.CursoNome,
|
&i.CursoNome,
|
||||||
|
|
@ -840,7 +848,7 @@ SET
|
||||||
pre_venda = $24,
|
pre_venda = $24,
|
||||||
atualizado_em = NOW()
|
atualizado_em = NOW()
|
||||||
WHERE id = $1
|
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 {
|
type UpdateAgendaParams struct {
|
||||||
|
|
@ -927,6 +935,7 @@ func (q *Queries) UpdateAgenda(ctx context.Context, arg UpdateAgendaParams) (Age
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -935,7 +944,7 @@ const updateAgendaStatus = `-- name: UpdateAgendaStatus :one
|
||||||
UPDATE agenda
|
UPDATE agenda
|
||||||
SET status = $2, atualizado_em = NOW()
|
SET status = $2, atualizado_em = NOW()
|
||||||
WHERE id = $1
|
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 {
|
type UpdateAgendaStatusParams struct {
|
||||||
|
|
@ -975,6 +984,7 @@ func (q *Queries) UpdateAgendaStatus(ctx context.Context, arg UpdateAgendaStatus
|
||||||
&i.CriadoEm,
|
&i.CriadoEm,
|
||||||
&i.AtualizadoEm,
|
&i.AtualizadoEm,
|
||||||
&i.Status,
|
&i.Status,
|
||||||
|
&i.LogisticaNotificacaoEnviadaEm,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
34
backend/internal/db/generated/agenda_supp.sql.go
Normal file
34
backend/internal/db/generated/agenda_supp.sql.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -73,13 +73,15 @@ func (q *Queries) CreateCarro(ctx context.Context, arg CreateCarroParams) (Logis
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCarro = `-- name: DeleteCarro :exec
|
const deleteCarro = `-- name: DeleteCarro :one
|
||||||
DELETE FROM logistica_carros WHERE id = $1
|
DELETE FROM logistica_carros WHERE id = $1 RETURNING agenda_id
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteCarro(ctx context.Context, id pgtype.UUID) error {
|
func (q *Queries) DeleteCarro(ctx context.Context, id pgtype.UUID) (pgtype.UUID, error) {
|
||||||
_, err := q.db.Exec(ctx, deleteCarro, id)
|
row := q.db.QueryRow(ctx, deleteCarro, id)
|
||||||
return err
|
var agenda_id pgtype.UUID
|
||||||
|
err := row.Scan(&agenda_id)
|
||||||
|
return agenda_id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCarroByID = `-- name: GetCarroByID :one
|
const getCarroByID = `-- name: GetCarroByID :one
|
||||||
|
|
|
||||||
|
|
@ -9,34 +9,35 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Agenda struct {
|
type Agenda struct {
|
||||||
ID pgtype.UUID `json:"id"`
|
ID pgtype.UUID `json:"id"`
|
||||||
UserID pgtype.UUID `json:"user_id"`
|
UserID pgtype.UUID `json:"user_id"`
|
||||||
FotID pgtype.UUID `json:"fot_id"`
|
FotID pgtype.UUID `json:"fot_id"`
|
||||||
DataEvento pgtype.Date `json:"data_evento"`
|
DataEvento pgtype.Date `json:"data_evento"`
|
||||||
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
TipoEventoID pgtype.UUID `json:"tipo_evento_id"`
|
||||||
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
ObservacoesEvento pgtype.Text `json:"observacoes_evento"`
|
||||||
LocalEvento pgtype.Text `json:"local_evento"`
|
LocalEvento pgtype.Text `json:"local_evento"`
|
||||||
Endereco pgtype.Text `json:"endereco"`
|
Endereco pgtype.Text `json:"endereco"`
|
||||||
Horario pgtype.Text `json:"horario"`
|
Horario pgtype.Text `json:"horario"`
|
||||||
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
QtdFormandos pgtype.Int4 `json:"qtd_formandos"`
|
||||||
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
QtdFotografos pgtype.Int4 `json:"qtd_fotografos"`
|
||||||
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
QtdRecepcionistas pgtype.Int4 `json:"qtd_recepcionistas"`
|
||||||
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
QtdCinegrafistas pgtype.Int4 `json:"qtd_cinegrafistas"`
|
||||||
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
QtdEstudios pgtype.Int4 `json:"qtd_estudios"`
|
||||||
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
QtdPontoFoto pgtype.Int4 `json:"qtd_ponto_foto"`
|
||||||
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
QtdPontoID pgtype.Int4 `json:"qtd_ponto_id"`
|
||||||
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
QtdPontoDecorado pgtype.Int4 `json:"qtd_ponto_decorado"`
|
||||||
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
QtdPontosLed pgtype.Int4 `json:"qtd_pontos_led"`
|
||||||
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
QtdPlataforma360 pgtype.Int4 `json:"qtd_plataforma_360"`
|
||||||
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
StatusProfissionais pgtype.Text `json:"status_profissionais"`
|
||||||
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
FotoFaltante pgtype.Int4 `json:"foto_faltante"`
|
||||||
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
RecepFaltante pgtype.Int4 `json:"recep_faltante"`
|
||||||
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
CineFaltante pgtype.Int4 `json:"cine_faltante"`
|
||||||
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
LogisticaObservacoes pgtype.Text `json:"logistica_observacoes"`
|
||||||
PreVenda pgtype.Bool `json:"pre_venda"`
|
PreVenda pgtype.Bool `json:"pre_venda"`
|
||||||
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
CriadoEm pgtype.Timestamptz `json:"criado_em"`
|
||||||
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
AtualizadoEm pgtype.Timestamptz `json:"atualizado_em"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
|
LogisticaNotificacaoEnviadaEm pgtype.Timestamp `json:"logistica_notificacao_enviada_em"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgendaEscala struct {
|
type AgendaEscala struct {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
10
backend/internal/db/queries/agenda_supp.sql
Normal file
10
backend/internal/db/queries/agenda_supp.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -29,8 +29,8 @@ SET motorista_id = COALESCE($2, motorista_id),
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: DeleteCarro :exec
|
-- name: DeleteCarro :one
|
||||||
DELETE FROM logistica_carros WHERE id = $1;
|
DELETE FROM logistica_carros WHERE id = $1 RETURNING agenda_id;
|
||||||
|
|
||||||
-- name: AddPassageiro :one
|
-- name: AddPassageiro :one
|
||||||
INSERT INTO logistica_passageiros (carro_id, profissional_id)
|
INSERT INTO logistica_passageiros (carro_id, profissional_id)
|
||||||
|
|
|
||||||
|
|
@ -351,7 +351,8 @@ CREATE TABLE IF NOT EXISTS agenda (
|
||||||
pre_venda BOOLEAN DEFAULT FALSE,
|
pre_venda BOOLEAN DEFAULT FALSE,
|
||||||
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
atualizado_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 (
|
CREATE TABLE IF NOT EXISTS agenda_profissionais (
|
||||||
|
|
@ -471,3 +472,10 @@ BEGIN
|
||||||
END IF;
|
END IF;
|
||||||
END $$;
|
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
|
||||||
|
$$;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ package logistica
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"photum-backend/internal/config"
|
"photum-backend/internal/config"
|
||||||
"photum-backend/internal/db/generated"
|
"photum-backend/internal/db/generated"
|
||||||
|
|
@ -64,6 +62,12 @@ func (s *Service) CreateCarro(ctx context.Context, input CreateCarroInput) (*gen
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset notification timestamp
|
||||||
|
if err := s.queries.ResetLogisticsNotificationTimestamp(ctx, params.AgendaID); err != nil {
|
||||||
|
// log error
|
||||||
|
}
|
||||||
|
|
||||||
return &carro, nil
|
return &carro, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,7 +84,19 @@ func (s *Service) DeleteCarro(ctx context.Context, id string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// UpdateCarroInput matches the update fields
|
||||||
|
|
@ -145,65 +161,67 @@ func (s *Service) AddPassageiro(ctx context.Context, carroID, profissionalID str
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification Logic
|
// Notification Logic - DISABLED (Moved to Manual Trigger)
|
||||||
go func() {
|
/*
|
||||||
bgCtx := context.Background()
|
go func() {
|
||||||
|
bgCtx := context.Background()
|
||||||
|
|
||||||
// 1. Get Car Details (Driver, Time, AgendaID)
|
// 1. Get Car Details (Driver, Time, AgendaID)
|
||||||
carro, err := s.queries.GetCarroByID(bgCtx, pgtype.UUID{Bytes: cID, Valid: true})
|
carro, err := s.queries.GetCarroByID(bgCtx, pgtype.UUID{Bytes: cID, Valid: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Logistica Notification] Erro ao buscar carro: %v", err)
|
log.Printf("[Logistica Notification] Erro ao buscar carro: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get Agenda Details (for Location)
|
// 2. Get Agenda Details (for Location)
|
||||||
// We have agenda_id in carro, but need to fetch details
|
// We have agenda_id in carro, but need to fetch details
|
||||||
agendaVal, err := s.queries.GetAgenda(bgCtx, carro.AgendaID)
|
agendaVal, err := s.queries.GetAgenda(bgCtx, carro.AgendaID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Logistica Notification] Erro ao buscar agenda: %v", err)
|
log.Printf("[Logistica Notification] Erro ao buscar agenda: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Get Professional (Passenger) Details
|
// 3. Get Professional (Passenger) Details
|
||||||
prof, err := s.queries.GetProfissionalByID(bgCtx, pgtype.UUID{Bytes: pID, Valid: true})
|
prof, err := s.queries.GetProfissionalByID(bgCtx, pgtype.UUID{Bytes: pID, Valid: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[Logistica Notification] Erro ao buscar passageiro: %v", err)
|
log.Printf("[Logistica Notification] Erro ao buscar passageiro: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if prof.Whatsapp.String == "" {
|
if prof.Whatsapp.String == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Format Message
|
// 4. Format Message
|
||||||
motorista := "A definir"
|
motorista := "A definir"
|
||||||
if carro.NomeMotorista.Valid {
|
if carro.NomeMotorista.Valid {
|
||||||
motorista = carro.NomeMotorista.String
|
motorista = carro.NomeMotorista.String
|
||||||
} else if carro.MotoristaNomeSistema.Valid {
|
} else if carro.MotoristaNomeSistema.Valid {
|
||||||
motorista = carro.MotoristaNomeSistema.String
|
motorista = carro.MotoristaNomeSistema.String
|
||||||
}
|
}
|
||||||
|
|
||||||
horarioSaida := "A combinar"
|
horarioSaida := "A combinar"
|
||||||
if carro.HorarioChegada.Valid {
|
if carro.HorarioChegada.Valid {
|
||||||
horarioSaida = carro.HorarioChegada.String
|
horarioSaida = carro.HorarioChegada.String
|
||||||
}
|
}
|
||||||
|
|
||||||
destino := "Local do Evento"
|
destino := "Local do Evento"
|
||||||
if agendaVal.LocalEvento.Valid {
|
if agendaVal.LocalEvento.Valid {
|
||||||
destino = agendaVal.LocalEvento.String
|
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.",
|
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,
|
prof.Nome,
|
||||||
motorista,
|
motorista,
|
||||||
horarioSaida,
|
horarioSaida,
|
||||||
destino,
|
destino,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil {
|
if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil {
|
||||||
log.Printf("[Logistica Notification] Falha ao enviar: %v", err)
|
log.Printf("[Logistica Notification] Falha ao enviar: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
*/
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import React, { useState, useEffect } from "react";
|
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 { 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 { useData } from "../contexts/DataContext";
|
||||||
import { UserRole } from "../types";
|
import { UserRole } from "../types";
|
||||||
|
|
||||||
interface EventLogisticsProps {
|
interface EventLogisticsProps {
|
||||||
agendaId: string;
|
agendaId: string;
|
||||||
|
token?: string;
|
||||||
|
isEditable?: boolean;
|
||||||
assignedProfessionals?: string[];
|
assignedProfessionals?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,9 +29,10 @@ interface PassengerWithOrder {
|
||||||
order: number;
|
order: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfessionals }) => {
|
const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, isEditable: propsIsEditable, assignedProfessionals }) => {
|
||||||
const { token, user } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const { professionals } = useData();
|
const { professionals, events } = useData();
|
||||||
|
const eventData = events.find(e => e.id === agendaId);
|
||||||
const [carros, setCarros] = useState<Carro[]>([]);
|
const [carros, setCarros] = useState<Carro[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [passengerOrders, setPassengerOrders] = useState<Record<string, Record<string, number>>>({});
|
const [passengerOrders, setPassengerOrders] = useState<Record<string, Record<string, number>>>({});
|
||||||
|
|
@ -39,7 +42,7 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
|
||||||
const [arrivalTime, setArrivalTime] = useState("07:00");
|
const [arrivalTime, setArrivalTime] = useState("07:00");
|
||||||
const [notes, setNotes] = useState("");
|
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
|
// Carregar ordens do localStorage ao montar o componente
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -175,12 +178,31 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ 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 (
|
return (
|
||||||
<div className="bg-white p-4 rounded-lg shadow space-y-4">
|
<div className="bg-white p-4 rounded-lg shadow space-y-4">
|
||||||
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Truck className="w-5 h-5 mr-2 text-orange-500" />
|
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
|
||||||
Logística de Transporte
|
<Truck className="w-5 h-5 mr-2 text-orange-500" />
|
||||||
</h3>
|
Logística de Transporte
|
||||||
|
</h3>
|
||||||
|
{isEditable && eventData?.logisticaNotificacaoEnviadaEm && (
|
||||||
|
<div className="text-sm text-green-700 bg-green-50 px-2 py-1 rounded border border-green-200">
|
||||||
|
Notificação enviada em: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Add Car Form - Only for Admins */}
|
{/* Add Car Form - Only for Admins */}
|
||||||
{isEditable && (
|
{isEditable && (
|
||||||
|
|
@ -306,6 +328,30 @@ const EventLogistics: React.FC<EventLogisticsProps> = ({ agendaId, assignedProfe
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Notification Button moved to bottom */}
|
||||||
|
{isEditable && (
|
||||||
|
<div className="flex flex-col items-center pt-4 border-t border-gray-100">
|
||||||
|
<button
|
||||||
|
onClick={handleNotifyLogistics}
|
||||||
|
disabled={!!eventData?.logisticaNotificacaoEnviadaEm}
|
||||||
|
className={`px-6 py-2.5 rounded-md flex items-center text-sm font-medium shadow-sm transition-colors w-full justify-center ${
|
||||||
|
eventData?.logisticaNotificacaoEnviadaEm
|
||||||
|
? "bg-gray-400 cursor-not-allowed text-white"
|
||||||
|
: "bg-green-600 hover:bg-green-700 text-white"
|
||||||
|
}`}
|
||||||
|
title={eventData?.logisticaNotificacaoEnviadaEm ? "Notificação já enviada" : "Confirmar logística e notificar equipe"}
|
||||||
|
>
|
||||||
|
<Send className="w-5 h-5 mr-2" />
|
||||||
|
{eventData?.logisticaNotificacaoEnviadaEm ? "Notificação Enviada" : "Finalizar Logística e Notificar Equipe"}
|
||||||
|
</button>
|
||||||
|
{eventData?.logisticaNotificacaoEnviadaEm && (
|
||||||
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
|
Última notificação: {new Date(eventData.logisticaNotificacaoEnviadaEm).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ const timeSlots = [
|
||||||
|
|
||||||
const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime }) => {
|
const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime }) => {
|
||||||
const { token, user } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const { professionals, events } = useData();
|
const { professionals, events, functions } = useData();
|
||||||
const [escalas, setEscalas] = useState<any[]>([]);
|
const [escalas, setEscalas] = useState<any[]>([]);
|
||||||
const [roles, setRoles] = useState<{ id: string; nome: string }[]>([]);
|
const roles = functions;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// New entry state
|
// New entry state
|
||||||
|
|
@ -80,9 +80,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (agendaId && token) {
|
if (agendaId && token) {
|
||||||
fetchEscalas();
|
fetchEscalas();
|
||||||
getFunctions().then(res => {
|
// Functions handled via context
|
||||||
if (res.data) setRoles(res.data);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [agendaId, token]);
|
}, [agendaId, token]);
|
||||||
|
|
||||||
|
|
@ -168,8 +166,10 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
ids.push(pid);
|
ids.push(pid);
|
||||||
allowedMap.set(pid, status);
|
allowedMap.set(pid, status);
|
||||||
|
|
||||||
if (p.funcaoId) {
|
// Use assigned role ID (handle both casing)
|
||||||
const r = roles.find(role => role.id === p.funcaoId);
|
const fId = p.funcaoId || p.funcao_id;
|
||||||
|
if (fId) {
|
||||||
|
const r = roles.find(role => role.id === fId);
|
||||||
if (r) assignedRoleMap.set(pid, r.nome);
|
if (r) assignedRoleMap.set(pid, r.nome);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +217,9 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
const isDisabled = isBusy || isPending;
|
const isDisabled = isBusy || isPending;
|
||||||
|
|
||||||
const assignedRole = assignedRoleMap.get(p.id);
|
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 = "";
|
let label = "";
|
||||||
if (isPending) label = "(Pendente de Aceite)";
|
if (isPending) label = "(Pendente de Aceite)";
|
||||||
|
|
@ -281,7 +283,9 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, a
|
||||||
// Ideally backend should return it, but for now we look up in global list if available
|
// 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 profData = professionals.find(p => p.id === item.profissional_id);
|
||||||
const assignedRole = assignedRoleMap.get(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 (
|
return (
|
||||||
<div key={item.id} className="flex flex-col p-2 hover:bg-gray-50 rounded border-b">
|
<div key={item.id} className="flex flex-col p-2 hover:bg-gray-50 rounded border-b">
|
||||||
|
|
|
||||||
|
|
@ -725,6 +725,7 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
funcaoId: a.funcao_id
|
funcaoId: a.funcao_id
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
|
logisticaNotificacaoEnviadaEm: e.logistica_notificacao_enviada_em,
|
||||||
}));
|
}));
|
||||||
setEvents(mappedEvents);
|
setEvents(mappedEvents);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,12 @@ const Finance: React.FC = () => {
|
||||||
// Default sort is grouped by FOT
|
// Default sort is grouped by FOT
|
||||||
if (!sortConfig) {
|
if (!sortConfig) {
|
||||||
return result.sort((a, b) => {
|
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();
|
return new Date(b.dataRaw || b.data).getTime() - new Date(a.dataRaw || a.data).getTime();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<ApiResponse<void>> {
|
||||||
|
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
|
* Cria um usuário pela interface administrativa
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ export interface EventData {
|
||||||
photographerIds: string[]; // IDs dos fotógrafos designados
|
photographerIds: string[]; // IDs dos fotógrafos designados
|
||||||
institutionId?: string; // ID da instituição vinculada (obrigatório)
|
institutionId?: string; // ID da instituição vinculada (obrigatório)
|
||||||
attendees?: number; // Número de pessoas participantes
|
attendees?: number; // Número de pessoas participantes
|
||||||
|
logisticaNotificacaoEnviadaEm?: string;
|
||||||
courseId?: string; // ID do curso/turma relacionado ao evento
|
courseId?: string; // ID do curso/turma relacionado ao evento
|
||||||
fotId?: string; // ID da Turma (FOT)
|
fotId?: string; // ID da Turma (FOT)
|
||||||
typeId?: string; // ID do Tipo de Evento (UUID)
|
typeId?: string; // ID do Tipo de Evento (UUID)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue