- Implementa envio de notificação WhatsApp ao aprovar evento ("Confirmado"), incluindo detalhes de logística (carro, motorista, passageiros) e endereço formatado.
- Adiciona coluna `funcao_id` em `agenda_profissionais` para distinguir a função específica do profissional no evento.
- Corrige bug de contagem duplicada na tabela de eventos para profissionais com múltiplas funções.
- Corrige validação ao aceitar convite para checar lotação apenas da função designada.
- Adiciona exibição da função (ex: Fotógrafo, Cinegrafista) na lista lateral do painel.
589 lines
21 KiB
Go
589 lines
21 KiB
Go
package agenda
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"time"
|
|
|
|
"photum-backend/internal/config"
|
|
"photum-backend/internal/db/generated"
|
|
"photum-backend/internal/notification"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
type Service struct {
|
|
queries *generated.Queries
|
|
notification *notification.Service
|
|
cfg *config.Config
|
|
}
|
|
|
|
func NewService(db *generated.Queries, notif *notification.Service, cfg *config.Config) *Service {
|
|
return &Service{
|
|
queries: db,
|
|
notification: notif,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
type CreateAgendaRequest struct {
|
|
FotID uuid.UUID `json:"fot_id"`
|
|
DataEvento time.Time `json:"data_evento"`
|
|
TipoEventoID uuid.UUID `json:"tipo_evento_id"`
|
|
ObservacoesEvento string `json:"observacoes_evento"`
|
|
LocalEvento string `json:"local_evento"`
|
|
Endereco string `json:"endereco"`
|
|
Horario string `json:"horario"`
|
|
QtdFormandos int32 `json:"qtd_formandos"`
|
|
QtdFotografos int32 `json:"qtd_fotografos"`
|
|
QtdRecepcionistas int32 `json:"qtd_recepcionistas"`
|
|
QtdCinegrafistas int32 `json:"qtd_cinegrafistas"`
|
|
QtdEstudios int32 `json:"qtd_estudios"`
|
|
QtdPontoFoto int32 `json:"qtd_ponto_foto"`
|
|
QtdPontoID int32 `json:"qtd_ponto_id"`
|
|
QtdPontoDecorado int32 `json:"qtd_ponto_decorado"`
|
|
QtdPontosLed int32 `json:"qtd_pontos_led"`
|
|
QtdPlataforma360 int32 `json:"qtd_plataforma_360"`
|
|
StatusProfissionais string `json:"status_profissionais"`
|
|
FotoFaltante int32 `json:"foto_faltante"`
|
|
RecepFaltante int32 `json:"recep_faltante"`
|
|
CineFaltante int32 `json:"cine_faltante"`
|
|
LogisticaObservacoes string `json:"logistica_observacoes"`
|
|
PreVenda bool `json:"pre_venda"`
|
|
}
|
|
|
|
type Assignment struct {
|
|
ProfessionalID string `json:"professional_id"`
|
|
Status string `json:"status"`
|
|
MotivoRejeicao *string `json:"motivo_rejeicao"`
|
|
}
|
|
|
|
type AgendaResponse struct {
|
|
generated.ListAgendasRow
|
|
ParsedAssignments []Assignment `json:"assignments"`
|
|
}
|
|
|
|
func (s *Service) CalculateStatus(fotoFaltante, recepFaltante, cineFaltante int32) string {
|
|
if fotoFaltante < 0 || recepFaltante < 0 || cineFaltante < 0 {
|
|
return "ERRO"
|
|
}
|
|
|
|
sum := fotoFaltante + recepFaltante + cineFaltante
|
|
if sum == 0 {
|
|
return "OK"
|
|
} else if sum > 0 {
|
|
return "FALTA"
|
|
}
|
|
|
|
return "ERRO"
|
|
}
|
|
|
|
func (s *Service) Create(ctx context.Context, userID uuid.UUID, req CreateAgendaRequest) (generated.Agenda, error) {
|
|
status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante)
|
|
|
|
params := generated.CreateAgendaParams{
|
|
FotID: pgtype.UUID{Bytes: req.FotID, Valid: true},
|
|
DataEvento: pgtype.Date{Time: req.DataEvento, Valid: true},
|
|
TipoEventoID: pgtype.UUID{Bytes: req.TipoEventoID, Valid: true},
|
|
ObservacoesEvento: pgtype.Text{String: req.ObservacoesEvento, Valid: req.ObservacoesEvento != ""},
|
|
LocalEvento: pgtype.Text{String: req.LocalEvento, Valid: req.LocalEvento != ""},
|
|
Endereco: pgtype.Text{String: req.Endereco, Valid: req.Endereco != ""},
|
|
Horario: pgtype.Text{String: req.Horario, Valid: req.Horario != ""},
|
|
QtdFormandos: pgtype.Int4{Int32: req.QtdFormandos, Valid: true},
|
|
QtdFotografos: pgtype.Int4{Int32: req.QtdFotografos, Valid: true},
|
|
QtdRecepcionistas: pgtype.Int4{Int32: req.QtdRecepcionistas, Valid: true},
|
|
QtdCinegrafistas: pgtype.Int4{Int32: req.QtdCinegrafistas, Valid: true},
|
|
QtdEstudios: pgtype.Int4{Int32: req.QtdEstudios, Valid: true},
|
|
QtdPontoFoto: pgtype.Int4{Int32: req.QtdPontoFoto, Valid: true},
|
|
QtdPontoID: pgtype.Int4{Int32: req.QtdPontoID, Valid: true},
|
|
QtdPontoDecorado: pgtype.Int4{Int32: req.QtdPontoDecorado, Valid: true},
|
|
QtdPontosLed: pgtype.Int4{Int32: req.QtdPontosLed, Valid: true},
|
|
QtdPlataforma360: pgtype.Int4{Int32: req.QtdPlataforma360, Valid: true},
|
|
StatusProfissionais: pgtype.Text{String: status, Valid: true},
|
|
FotoFaltante: pgtype.Int4{Int32: req.FotoFaltante, Valid: true},
|
|
RecepFaltante: pgtype.Int4{Int32: req.RecepFaltante, Valid: true},
|
|
CineFaltante: pgtype.Int4{Int32: req.CineFaltante, Valid: true},
|
|
LogisticaObservacoes: pgtype.Text{String: req.LogisticaObservacoes, Valid: req.LogisticaObservacoes != ""},
|
|
PreVenda: pgtype.Bool{Bool: req.PreVenda, Valid: true},
|
|
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
|
}
|
|
|
|
return s.queries.CreateAgenda(ctx, params)
|
|
}
|
|
|
|
func (s *Service) List(ctx context.Context, userID uuid.UUID, role string) ([]AgendaResponse, error) {
|
|
var rows []generated.ListAgendasRow
|
|
var err error
|
|
|
|
// If role is CLIENT (cliente), filter by userID
|
|
if role == "cliente" || role == "EVENT_OWNER" {
|
|
listRows, err := s.queries.ListAgendasByUser(ctx, pgtype.UUID{Bytes: userID, Valid: true})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Convert ListAgendasByUserRow to ListAgendasRow manually
|
|
for _, r := range listRows {
|
|
rows = append(rows, generated.ListAgendasRow{
|
|
ID: r.ID,
|
|
UserID: r.UserID,
|
|
FotID: r.FotID,
|
|
DataEvento: r.DataEvento,
|
|
TipoEventoID: r.TipoEventoID,
|
|
ObservacoesEvento: r.ObservacoesEvento,
|
|
LocalEvento: r.LocalEvento,
|
|
Endereco: r.Endereco,
|
|
Horario: r.Horario,
|
|
QtdFormandos: r.QtdFormandos,
|
|
QtdFotografos: r.QtdFotografos,
|
|
QtdRecepcionistas: r.QtdRecepcionistas,
|
|
QtdCinegrafistas: r.QtdCinegrafistas,
|
|
QtdEstudios: r.QtdEstudios,
|
|
QtdPontoFoto: r.QtdPontoFoto,
|
|
QtdPontoID: r.QtdPontoID,
|
|
QtdPontoDecorado: r.QtdPontoDecorado,
|
|
QtdPontosLed: r.QtdPontosLed,
|
|
QtdPlataforma360: r.QtdPlataforma360,
|
|
StatusProfissionais: r.StatusProfissionais,
|
|
FotoFaltante: r.FotoFaltante,
|
|
RecepFaltante: r.RecepFaltante,
|
|
CineFaltante: r.CineFaltante,
|
|
LogisticaObservacoes: r.LogisticaObservacoes,
|
|
PreVenda: r.PreVenda,
|
|
CriadoEm: r.CriadoEm,
|
|
AtualizadoEm: r.AtualizadoEm,
|
|
Status: r.Status,
|
|
FotNumero: r.FotNumero,
|
|
Instituicao: r.Instituicao,
|
|
CursoNome: r.CursoNome,
|
|
EmpresaNome: r.EmpresaNome,
|
|
EmpresaID: r.EmpresaID,
|
|
AnoSemestre: r.AnoSemestre,
|
|
ObservacoesFot: r.ObservacoesFot,
|
|
TipoEventoNome: r.TipoEventoNome,
|
|
AssignedProfessionals: r.AssignedProfessionals,
|
|
})
|
|
}
|
|
} else {
|
|
rows, err = s.queries.ListAgendas(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var response []AgendaResponse
|
|
for _, row := range rows {
|
|
var assignments []Assignment
|
|
if row.AssignedProfessionals != nil {
|
|
bytes, ok := row.AssignedProfessionals.([]byte)
|
|
if !ok {
|
|
str, ok := row.AssignedProfessionals.(string)
|
|
if ok {
|
|
bytes = []byte(str)
|
|
}
|
|
}
|
|
if bytes != nil {
|
|
json.Unmarshal(bytes, &assignments)
|
|
}
|
|
}
|
|
response = append(response, AgendaResponse{
|
|
ListAgendasRow: row,
|
|
ParsedAssignments: assignments,
|
|
})
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (s *Service) Get(ctx context.Context, id uuid.UUID) (generated.Agenda, error) {
|
|
return s.queries.GetAgenda(ctx, pgtype.UUID{Bytes: id, Valid: true})
|
|
}
|
|
|
|
func (s *Service) Update(ctx context.Context, id uuid.UUID, req CreateAgendaRequest) (generated.Agenda, error) {
|
|
if req.FotID == uuid.Nil {
|
|
return generated.Agenda{}, fmt.Errorf("FOT ID inválido ou não informado")
|
|
}
|
|
if req.TipoEventoID == uuid.Nil {
|
|
return generated.Agenda{}, fmt.Errorf("Tipo de Evento ID inválido ou não informado")
|
|
}
|
|
|
|
status := s.CalculateStatus(req.FotoFaltante, req.RecepFaltante, req.CineFaltante)
|
|
|
|
params := generated.UpdateAgendaParams{
|
|
ID: pgtype.UUID{Bytes: id, Valid: true},
|
|
FotID: pgtype.UUID{Bytes: req.FotID, Valid: true},
|
|
DataEvento: pgtype.Date{Time: req.DataEvento, Valid: true},
|
|
TipoEventoID: pgtype.UUID{Bytes: req.TipoEventoID, Valid: true},
|
|
ObservacoesEvento: pgtype.Text{String: req.ObservacoesEvento, Valid: req.ObservacoesEvento != ""},
|
|
LocalEvento: pgtype.Text{String: req.LocalEvento, Valid: req.LocalEvento != ""},
|
|
Endereco: pgtype.Text{String: req.Endereco, Valid: req.Endereco != ""},
|
|
Horario: pgtype.Text{String: req.Horario, Valid: req.Horario != ""},
|
|
QtdFormandos: pgtype.Int4{Int32: req.QtdFormandos, Valid: true},
|
|
QtdFotografos: pgtype.Int4{Int32: req.QtdFotografos, Valid: true},
|
|
QtdRecepcionistas: pgtype.Int4{Int32: req.QtdRecepcionistas, Valid: true},
|
|
QtdCinegrafistas: pgtype.Int4{Int32: req.QtdCinegrafistas, Valid: true},
|
|
QtdEstudios: pgtype.Int4{Int32: req.QtdEstudios, Valid: true},
|
|
QtdPontoFoto: pgtype.Int4{Int32: req.QtdPontoFoto, Valid: true},
|
|
QtdPontoID: pgtype.Int4{Int32: req.QtdPontoID, Valid: true},
|
|
QtdPontoDecorado: pgtype.Int4{Int32: req.QtdPontoDecorado, Valid: true},
|
|
QtdPontosLed: pgtype.Int4{Int32: req.QtdPontosLed, Valid: true},
|
|
QtdPlataforma360: pgtype.Int4{Int32: req.QtdPlataforma360, Valid: true},
|
|
StatusProfissionais: pgtype.Text{String: status, Valid: true},
|
|
FotoFaltante: pgtype.Int4{Int32: req.FotoFaltante, Valid: true},
|
|
RecepFaltante: pgtype.Int4{Int32: req.RecepFaltante, Valid: true},
|
|
CineFaltante: pgtype.Int4{Int32: req.CineFaltante, Valid: true},
|
|
LogisticaObservacoes: pgtype.Text{String: req.LogisticaObservacoes, Valid: req.LogisticaObservacoes != ""},
|
|
PreVenda: pgtype.Bool{Bool: req.PreVenda, Valid: true},
|
|
}
|
|
return s.queries.UpdateAgenda(ctx, params)
|
|
}
|
|
|
|
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
return s.queries.DeleteAgenda(ctx, pgtype.UUID{Bytes: id, Valid: true})
|
|
}
|
|
|
|
func (s *Service) AssignProfessional(ctx context.Context, agendaID uuid.UUID, profID uuid.UUID, funcaoID *uuid.UUID) error {
|
|
params := generated.AssignProfessionalParams{
|
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
ProfissionalID: pgtype.UUID{Bytes: profID, Valid: true},
|
|
}
|
|
|
|
if funcaoID != nil {
|
|
params.FuncaoID = pgtype.UUID{Bytes: *funcaoID, Valid: true}
|
|
} else {
|
|
params.FuncaoID = pgtype.UUID{Valid: false}
|
|
}
|
|
|
|
if err := s.queries.AssignProfessional(ctx, params); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Notification Logic (Async or Sync - staying Sync but safe for now)
|
|
go func() {
|
|
// Create a detached context for the notification to avoid cancellation if the HTTP request ends
|
|
// Note: Ideally use a proper background context with timeout
|
|
bgCtx := context.Background()
|
|
|
|
// 1. Get Agenda Details
|
|
agenda, err := s.queries.GetAgenda(bgCtx, pgtype.UUID{Bytes: agendaID, Valid: true})
|
|
if err != nil {
|
|
log.Printf("[Notification] Erro ao buscar dados da agenda para notificação: %v", err)
|
|
return
|
|
}
|
|
|
|
// 2. Get Professional Details
|
|
// We use GetProfissionalByID which returns the struct we need
|
|
prof, err := s.queries.GetProfissionalByID(bgCtx, pgtype.UUID{Bytes: profID, Valid: true})
|
|
if err != nil {
|
|
log.Printf("[Notification] Erro ao buscar dados do profissional para notificação: %v", err)
|
|
return
|
|
}
|
|
|
|
if prof.Whatsapp.String == "" {
|
|
log.Printf("[Notification] Profissional %s sem WhatsApp cadastrado. Ignorando.", prof.Nome)
|
|
return
|
|
}
|
|
|
|
// 3. Get Event Type Details to show the name
|
|
// We need to fetch the TipoEvento name
|
|
var tipoEventoNome string = "Evento"
|
|
if agenda.TipoEventoID.Valid {
|
|
tipoEvento, err := s.queries.GetTipoEventoByID(bgCtx, pgtype.UUID{Bytes: agenda.TipoEventoID.Bytes, Valid: true})
|
|
if err == nil {
|
|
tipoEventoNome = tipoEvento.Nome
|
|
} else {
|
|
log.Printf("[Notification] Erro ao buscar tipo de evento: %v", err)
|
|
}
|
|
}
|
|
|
|
// 4. Format Message
|
|
dataEvento := "Data a definir"
|
|
if agenda.DataEvento.Valid {
|
|
dataEvento = agenda.DataEvento.Time.Format("02/01/2006")
|
|
}
|
|
|
|
horario := "Horário a definir"
|
|
if agenda.Horario.Valid && agenda.Horario.String != "" {
|
|
horario = agenda.Horario.String
|
|
}
|
|
|
|
local := "Local a definir"
|
|
if agenda.LocalEvento.Valid && agenda.LocalEvento.String != "" {
|
|
local = agenda.LocalEvento.String
|
|
}
|
|
|
|
if agenda.Endereco.Valid && agenda.Endereco.String != "" {
|
|
local += fmt.Sprintf(" (%s)", agenda.Endereco.String)
|
|
}
|
|
|
|
// Use configured FRONTEND_URL or default to localhost
|
|
baseUrl := "http://localhost:3000"
|
|
if s.cfg != nil && s.cfg.FrontendURL != "" {
|
|
baseUrl = s.cfg.FrontendURL
|
|
}
|
|
|
|
// Refined Message:
|
|
msg := fmt.Sprintf("Olá *%s*! 📸\n\nVocê foi escalado(a) para um evento no dia *%s*.\n\n*Tipo:* %s\n*Horário:* %s\n*Local:* %s\n\nAcesse seu painel na Photum para conferir os detalhes e confirmar sua presença.\n%s/entrar",
|
|
prof.Nome,
|
|
dataEvento,
|
|
tipoEventoNome,
|
|
horario,
|
|
local,
|
|
baseUrl,
|
|
)
|
|
|
|
if err := s.notification.SendWhatsApp(prof.Whatsapp.String, msg); err != nil {
|
|
log.Printf("[Notification] Falha ao enviar WhatsApp para %s: %v", prof.Nome, err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) RemoveProfessional(ctx context.Context, agendaID uuid.UUID, profID uuid.UUID) error {
|
|
params := generated.RemoveProfessionalParams{
|
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
ProfissionalID: pgtype.UUID{Bytes: profID, Valid: true},
|
|
}
|
|
return s.queries.RemoveProfessional(ctx, params)
|
|
}
|
|
|
|
func (s *Service) GetAgendaProfessionals(ctx context.Context, agendaID uuid.UUID) ([]generated.GetAgendaProfessionalsRow, error) {
|
|
return s.queries.GetAgendaProfessionals(ctx, pgtype.UUID{Bytes: agendaID, Valid: true})
|
|
}
|
|
|
|
func (s *Service) UpdateStatus(ctx context.Context, agendaID uuid.UUID, status string) (generated.Agenda, error) {
|
|
params := generated.UpdateAgendaStatusParams{
|
|
ID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
Status: pgtype.Text{String: status, Valid: true},
|
|
}
|
|
|
|
agenda, err := s.queries.UpdateAgendaStatus(ctx, params)
|
|
if err != nil {
|
|
return generated.Agenda{}, err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
return agenda, nil
|
|
}
|
|
|
|
func (s *Service) UpdateAssignmentStatus(ctx context.Context, agendaID, professionalID uuid.UUID, status string, reason string) error {
|
|
// Conflict Validation on Accept
|
|
if status == "ACEITO" {
|
|
// 1. Get Current Agenda to know Date/Time
|
|
agenda, err := s.queries.GetAgenda(ctx, pgtype.UUID{Bytes: agendaID, Valid: true})
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao buscar agenda para validação: %v", err)
|
|
}
|
|
|
|
if agenda.DataEvento.Valid {
|
|
// 2. Check for other confirmed events on the same date
|
|
// Exclude current agenda ID from check
|
|
conflicts, err := s.queries.CheckProfessionalBusyDate(ctx, generated.CheckProfessionalBusyDateParams{
|
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
|
DataEvento: agenda.DataEvento,
|
|
ID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("erro ao verificar disponibilidade: %v", err)
|
|
}
|
|
|
|
if len(conflicts) > 0 {
|
|
// 3. Check time overlap
|
|
currentStart := parseTimeMinutes(agenda.Horario.String)
|
|
currentEnd := currentStart + 240 // Assume 4 hours duration
|
|
|
|
for _, c := range conflicts {
|
|
conflictStart := parseTimeMinutes(c.Horario.String)
|
|
conflictEnd := conflictStart + 240
|
|
|
|
// Check overlap: StartA < EndB && StartB < EndA
|
|
if currentStart < conflictEnd && conflictStart < currentEnd {
|
|
return fmt.Errorf("conflito de horário: profissional já confirmou presença em outro evento às %s", c.Horario.String)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
params := generated.UpdateAssignmentStatusParams{
|
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
|
Status: pgtype.Text{String: status, Valid: true},
|
|
MotivoRejeicao: pgtype.Text{String: reason, Valid: reason != ""},
|
|
}
|
|
_, err := s.queries.UpdateAssignmentStatus(ctx, params)
|
|
return err
|
|
}
|
|
|
|
// Helper for time parsing (HH:MM)
|
|
func parseTimeMinutes(t string) int {
|
|
if len(t) < 5 {
|
|
return 0 // Default or Error
|
|
}
|
|
h, _ := strconv.Atoi(t[0:2])
|
|
m, _ := strconv.Atoi(t[3:5])
|
|
return h*60 + m
|
|
}
|
|
|
|
func (s *Service) UpdateAssignmentPosition(ctx context.Context, agendaID, professionalID uuid.UUID, posicao string) error {
|
|
params := generated.UpdateAssignmentPositionParams{
|
|
AgendaID: pgtype.UUID{Bytes: agendaID, Valid: true},
|
|
ProfissionalID: pgtype.UUID{Bytes: professionalID, Valid: true},
|
|
Posicao: pgtype.Text{String: posicao, Valid: true},
|
|
}
|
|
_, err := s.queries.UpdateAssignmentPosition(ctx, params)
|
|
return err
|
|
}
|
|
|
|
func (s *Service) ListAvailableProfessionals(ctx context.Context, date time.Time) ([]generated.ListAvailableProfessionalsForDateRow, error) {
|
|
return s.queries.ListAvailableProfessionalsForDate(ctx, pgtype.Date{Time: date, Valid: true})
|
|
}
|