Merge d2adf76d34 into 94908ead00
This commit is contained in:
commit
8db41acb46
|
|
@ -340,6 +340,22 @@ namespace line_gestao_api.Controllers
|
|||
return NoContent();
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ DELETE: /api/mureg/{id}
|
||||
// Exclui registro MUREG
|
||||
// ==========================================================
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var entity = await _db.MuregLines.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (entity == null) return NotFound();
|
||||
|
||||
_db.MuregLines.Remove(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// ✅ POST: /api/mureg/import-excel (mantido)
|
||||
// ==========================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Dtos;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace line_gestao_api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/notifications")]
|
||||
[Authorize]
|
||||
public class NotificationsController : ControllerBase
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
|
||||
public NotificationsController(AppDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[HttpGet("/notifications")]
|
||||
public async Task<ActionResult<List<NotificationDto>>> GetNotifications()
|
||||
{
|
||||
var query = _db.Notifications.AsNoTracking();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(n => n.Data)
|
||||
.Select(n => new NotificationDto
|
||||
{
|
||||
Id = n.Id,
|
||||
Tipo = n.Tipo,
|
||||
Titulo = n.Titulo,
|
||||
Mensagem = n.Mensagem,
|
||||
Data = n.Data,
|
||||
ReferenciaData = n.ReferenciaData,
|
||||
DiasParaVencer = n.DiasParaVencer,
|
||||
Lida = n.Lida,
|
||||
LidaEm = n.LidaEm,
|
||||
VigenciaLineId = n.VigenciaLineId,
|
||||
Cliente = n.Cliente,
|
||||
Linha = n.Linha
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}/read")]
|
||||
[HttpPatch("/notifications/{id:guid}/read")]
|
||||
public async Task<IActionResult> MarkAsRead(Guid id)
|
||||
{
|
||||
var notification = await _db.Notifications
|
||||
.FirstOrDefaultAsync(n => n.Id == id);
|
||||
|
||||
if (notification is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!notification.Lida)
|
||||
{
|
||||
notification.Lida = true;
|
||||
notification.LidaEm = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,6 +27,9 @@ public class AppDbContext : DbContext
|
|||
// ✅ tabela TROCA DE NÚMERO
|
||||
public DbSet<TrocaNumeroLine> TrocaNumeroLines => Set<TrocaNumeroLine>();
|
||||
|
||||
// ✅ tabela NOTIFICAÇÕES
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
|
@ -127,5 +130,28 @@ public class AppDbContext : DbContext
|
|||
e.HasIndex(x => x.ICCID);
|
||||
e.HasIndex(x => x.DataTroca);
|
||||
});
|
||||
|
||||
// =========================
|
||||
// ✅ NOTIFICAÇÕES
|
||||
// =========================
|
||||
modelBuilder.Entity<Notification>(e =>
|
||||
{
|
||||
e.HasIndex(x => x.DedupKey).IsUnique();
|
||||
e.HasIndex(x => x.UserId);
|
||||
e.HasIndex(x => x.Cliente);
|
||||
e.HasIndex(x => x.Lida);
|
||||
e.HasIndex(x => x.Data);
|
||||
e.HasIndex(x => x.VigenciaLineId);
|
||||
|
||||
e.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
e.HasOne(x => x.VigenciaLine)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.VigenciaLineId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
namespace line_gestao_api.Dtos;
|
||||
|
||||
public class NotificationDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Tipo { get; set; } = string.Empty;
|
||||
public string Titulo { get; set; } = string.Empty;
|
||||
public string Mensagem { get; set; } = string.Empty;
|
||||
public DateTime Data { get; set; }
|
||||
public DateTime? ReferenciaData { get; set; }
|
||||
public int? DiasParaVencer { get; set; }
|
||||
public bool Lida { get; set; }
|
||||
public DateTime? LidaEm { get; set; }
|
||||
public Guid? VigenciaLineId { get; set; }
|
||||
public string? Cliente { get; set; }
|
||||
public string? Linha { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,567 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using line_gestao_api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260205120000_AddNotifications")]
|
||||
partial class AddNotifications
|
||||
{
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Aparelho")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("FormaPagamento")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("FranquiaLine")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("FranquiaVivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal?>("Lucro")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int?>("QtdLinhas")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Tipo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2)
|
||||
.HasColumnType("character varying(2)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("ValorContratoLine")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("ValorContratoVivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("Item");
|
||||
|
||||
b.HasIndex("Tipo");
|
||||
|
||||
b.HasIndex("Tipo", "Cliente");
|
||||
|
||||
b.ToTable("billing_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Cedente")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<string>("Chip")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Conta")
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataBloqueio")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataEntregaCliente")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataEntregaOpera")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<decimal?>("Desconto")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("FranquiaGestao")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("FranquiaLine")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("FranquiaVivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("GestaoVozDados")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<decimal?>("LocacaoAp")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("Lucro")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Modalidade")
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<string>("PlanoContrato")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<decimal?>("Skeelo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("Skil")
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<string>("Solicitante")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("character varying(150)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Usuario")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<decimal?>("ValorContratoLine")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("ValorContratoVivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("ValorPlanoVivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("VencConta")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<decimal?>("VivoGestaoDispositivo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("VivoNewsPlus")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<decimal?>("VivoTravelMundo")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Chip");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("Linha")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Skil");
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("Usuario");
|
||||
|
||||
b.ToTable("MobileLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataDaMureg")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ICCID")
|
||||
.HasMaxLength(40)
|
||||
.HasColumnType("character varying(40)");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("LinhaAntiga")
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("LinhaNova")
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<Guid>("MobileLineId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ICCID");
|
||||
|
||||
b.HasIndex("Item");
|
||||
|
||||
b.HasIndex("LinhaAntiga");
|
||||
|
||||
b.HasIndex("LinhaNova");
|
||||
|
||||
b.HasIndex("MobileLineId");
|
||||
|
||||
b.ToTable("MuregLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.TrocaNumeroLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataTroca")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ICCID")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("LinhaAntiga")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("LinhaNova")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Motivo")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Observacao")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DataTroca");
|
||||
|
||||
b.HasIndex("ICCID");
|
||||
|
||||
b.HasIndex("Item");
|
||||
|
||||
b.HasIndex("LinhaAntiga");
|
||||
|
||||
b.HasIndex("LinhaNova");
|
||||
|
||||
b.ToTable("TrocaNumeroLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(120)
|
||||
.HasColumnType("character varying(120)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Data")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DedupKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("DiasParaVencer")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Lida")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LidaEm")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Mensagem")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("ReferenciaData")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Tipo")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Titulo")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Usuario")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("VigenciaLineId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("Data");
|
||||
|
||||
b.HasIndex("DedupKey")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Lida");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("VigenciaLineId");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Celular")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Cpf")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DataNascimento")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Endereco")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Rg")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TelefoneFixo")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("Cpf");
|
||||
|
||||
b.HasIndex("Email");
|
||||
|
||||
b.HasIndex("Item");
|
||||
|
||||
b.HasIndex("Linha");
|
||||
|
||||
b.ToTable("UserDatas");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.VigenciaLine", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Conta")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DtEfetivacaoServico")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DtTerminoFidelizacao")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Item")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PlanoContrato")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("Total")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Usuario")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("DtTerminoFidelizacao");
|
||||
|
||||
b.HasIndex("Item");
|
||||
|
||||
b.HasIndex("Linha");
|
||||
|
||||
b.ToTable("VigenciaLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("line_gestao_api.Models.VigenciaLine", "VigenciaLine")
|
||||
.WithMany()
|
||||
.HasForeignKey("VigenciaLineId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("User");
|
||||
|
||||
b.Navigation("VigenciaLine");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
||||
.WithMany("Muregs")
|
||||
.HasForeignKey("MobileLineId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MobileLine");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
|
||||
{
|
||||
b.Navigation("Muregs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace line_gestao_api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotifications : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Tipo = table.Column<string>(type: "text", nullable: false),
|
||||
Titulo = table.Column<string>(type: "text", nullable: false),
|
||||
Mensagem = table.Column<string>(type: "text", nullable: false),
|
||||
Data = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
ReferenciaData = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
DiasParaVencer = table.Column<int>(type: "integer", nullable: true),
|
||||
Lida = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LidaEm = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
DedupKey = table.Column<string>(type: "text", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
VigenciaLineId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Usuario = table.Column<string>(type: "text", nullable: true),
|
||||
Cliente = table.Column<string>(type: "text", nullable: true),
|
||||
Linha = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notifications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Notifications_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Notifications_VigenciaLines_VigenciaLineId",
|
||||
column: x => x.VigenciaLineId,
|
||||
principalTable: "VigenciaLines",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_Cliente",
|
||||
table: "Notifications",
|
||||
column: "Cliente");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_Data",
|
||||
table: "Notifications",
|
||||
column: "Data");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_DedupKey",
|
||||
table: "Notifications",
|
||||
column: "DedupKey",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_Lida",
|
||||
table: "Notifications",
|
||||
column: "Lida");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_UserId",
|
||||
table: "Notifications",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_VigenciaLineId",
|
||||
table: "Notifications",
|
||||
column: "VigenciaLineId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notifications");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -348,6 +348,76 @@ namespace line_gestao_api.Migrations
|
|||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Cliente")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("Data")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DedupKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("DiasParaVencer")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Linha")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Lida")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("LidaEm")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Mensagem")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("ReferenciaData")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Tipo")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Titulo")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Usuario")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid?>("VigenciaLineId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Cliente");
|
||||
|
||||
b.HasIndex("Data");
|
||||
|
||||
b.HasIndex("DedupKey")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Lida");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("VigenciaLineId");
|
||||
|
||||
b.ToTable("Notifications");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.UserData", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -457,6 +527,23 @@ namespace line_gestao_api.Migrations
|
|||
b.ToTable("VigenciaLines");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.Notification", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.HasOne("line_gestao_api.Models.VigenciaLine", "VigenciaLine")
|
||||
.WithMany()
|
||||
.HasForeignKey("VigenciaLineId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("User");
|
||||
|
||||
b.Navigation("VigenciaLine");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
|
||||
{
|
||||
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace line_gestao_api.Models;
|
||||
|
||||
public class Notification
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
public string Tipo { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Titulo { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Mensagem { get; set; } = string.Empty;
|
||||
|
||||
public DateTime Data { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? ReferenciaData { get; set; }
|
||||
|
||||
public int? DiasParaVencer { get; set; }
|
||||
|
||||
public bool Lida { get; set; }
|
||||
|
||||
public DateTime? LidaEm { get; set; }
|
||||
|
||||
[Required]
|
||||
public string DedupKey { get; set; } = string.Empty;
|
||||
|
||||
public Guid? UserId { get; set; }
|
||||
public User? User { get; set; }
|
||||
|
||||
public Guid? VigenciaLineId { get; set; }
|
||||
public VigenciaLine? VigenciaLine { get; set; }
|
||||
|
||||
public string? Usuario { get; set; }
|
||||
public string? Cliente { get; set; }
|
||||
public string? Linha { get; set; }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Text;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Services;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
|
@ -57,6 +58,9 @@ builder.Services
|
|||
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
builder.Services.Configure<NotificationOptions>(builder.Configuration.GetSection("Notifications"));
|
||||
builder.Services.AddHostedService<VigenciaNotificationBackgroundService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace line_gestao_api.Services;
|
||||
|
||||
public class NotificationOptions
|
||||
{
|
||||
public int CheckIntervalMinutes { get; set; } = 60;
|
||||
public List<int> ReminderDays { get; set; } = new() { 30, 15, 7 };
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
using System.Globalization;
|
||||
using line_gestao_api.Data;
|
||||
using line_gestao_api.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace line_gestao_api.Services;
|
||||
|
||||
public class VigenciaNotificationBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ILogger<VigenciaNotificationBackgroundService> _logger;
|
||||
private readonly NotificationOptions _options;
|
||||
|
||||
public VigenciaNotificationBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<NotificationOptions> options,
|
||||
ILogger<VigenciaNotificationBackgroundService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var intervalMinutes = _options.CheckIntervalMinutes <= 0 ? 60 : _options.CheckIntervalMinutes;
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(intervalMinutes));
|
||||
|
||||
await RunOnceAsync(stoppingToken);
|
||||
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
await RunOnceAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunOnceAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
if (!await TableExistsAsync(db, "Notifications", stoppingToken))
|
||||
{
|
||||
_logger.LogWarning("Tabela Notifications ainda não existe. Aguardando migrations.");
|
||||
return;
|
||||
}
|
||||
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var reminderDays = _options.ReminderDays
|
||||
.Distinct()
|
||||
.Where(d => d > 0)
|
||||
.OrderBy(d => d)
|
||||
.ToList();
|
||||
|
||||
var users = await db.Users.AsNoTracking()
|
||||
.Select(u => new { u.Id, u.Name, u.Email })
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
var userByName = users
|
||||
.Where(u => !string.IsNullOrWhiteSpace(u.Name))
|
||||
.ToDictionary(u => u.Name.Trim().ToLowerInvariant(), u => u.Id);
|
||||
|
||||
var userByEmail = users
|
||||
.Where(u => !string.IsNullOrWhiteSpace(u.Email))
|
||||
.ToDictionary(u => u.Email.Trim().ToLowerInvariant(), u => u.Id);
|
||||
|
||||
var vigencias = await db.VigenciaLines.AsNoTracking()
|
||||
.Where(v => v.DtTerminoFidelizacao != null)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
var candidates = new List<Notification>();
|
||||
foreach (var vigencia in vigencias)
|
||||
{
|
||||
if (vigencia.DtTerminoFidelizacao is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var endDate = vigencia.DtTerminoFidelizacao.Value.Date;
|
||||
var usuario = vigencia.Usuario?.Trim();
|
||||
var cliente = vigencia.Cliente?.Trim();
|
||||
var linha = vigencia.Linha?.Trim();
|
||||
var usuarioKey = usuario?.ToLowerInvariant();
|
||||
|
||||
Guid? userId = null;
|
||||
if (!string.IsNullOrWhiteSpace(usuarioKey))
|
||||
{
|
||||
if (userByEmail.TryGetValue(usuarioKey, out var matchedByEmail))
|
||||
{
|
||||
userId = matchedByEmail;
|
||||
}
|
||||
else if (userByName.TryGetValue(usuarioKey, out var matchedByName))
|
||||
{
|
||||
userId = matchedByName;
|
||||
}
|
||||
}
|
||||
|
||||
if (endDate < today)
|
||||
{
|
||||
var notification = BuildNotification(
|
||||
tipo: "Vencido",
|
||||
titulo: $"Linha vencida{FormatLinha(linha)}",
|
||||
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} venceu em {endDate:dd/MM/yyyy}.",
|
||||
referenciaData: endDate,
|
||||
diasParaVencer: 0,
|
||||
userId: userId,
|
||||
usuario: usuario,
|
||||
cliente: cliente,
|
||||
linha: linha,
|
||||
vigenciaLineId: vigencia.Id);
|
||||
|
||||
candidates.Add(notification);
|
||||
continue;
|
||||
}
|
||||
|
||||
var daysUntil = (endDate - today).Days;
|
||||
if (reminderDays.Contains(daysUntil))
|
||||
{
|
||||
var notification = BuildNotification(
|
||||
tipo: "AVencer",
|
||||
titulo: $"Linha a vencer em {daysUntil} dias{FormatLinha(linha)}",
|
||||
mensagem: $"A linha{FormatLinha(linha)} do cliente {cliente ?? "(sem cliente)"} vence em {endDate:dd/MM/yyyy}.",
|
||||
referenciaData: endDate,
|
||||
diasParaVencer: daysUntil,
|
||||
userId: userId,
|
||||
usuario: usuario,
|
||||
cliente: cliente,
|
||||
linha: linha,
|
||||
vigenciaLineId: vigencia.Id);
|
||||
|
||||
candidates.Add(notification);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dedupKeys = candidates.Select(c => c.DedupKey).Distinct().ToList();
|
||||
var existingKeys = await db.Notifications.AsNoTracking()
|
||||
.Where(n => dedupKeys.Contains(n.DedupKey))
|
||||
.Select(n => n.DedupKey)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
var existingSet = new HashSet<string>(existingKeys);
|
||||
var toInsert = candidates
|
||||
.Where(c => !existingSet.Contains(c.DedupKey))
|
||||
.ToList();
|
||||
|
||||
if (toInsert.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await db.Notifications.AddRangeAsync(toInsert, stoppingToken);
|
||||
await db.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao gerar notificações de vigência.");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> TableExistsAsync(AppDbContext db, string tableName, CancellationToken stoppingToken)
|
||||
{
|
||||
if (!db.Database.IsRelational())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var connection = db.Database.GetDbConnection();
|
||||
if (connection.State != System.Data.ConnectionState.Open)
|
||||
{
|
||||
await connection.OpenAsync(stoppingToken);
|
||||
}
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = @tableName)";
|
||||
var parameter = command.CreateParameter();
|
||||
parameter.ParameterName = "tableName";
|
||||
parameter.Value = tableName;
|
||||
command.Parameters.Add(parameter);
|
||||
|
||||
var result = await command.ExecuteScalarAsync(stoppingToken);
|
||||
return result is bool exists && exists;
|
||||
}
|
||||
|
||||
private static Notification BuildNotification(
|
||||
string tipo,
|
||||
string titulo,
|
||||
string mensagem,
|
||||
DateTime referenciaData,
|
||||
int diasParaVencer,
|
||||
Guid? userId,
|
||||
string? usuario,
|
||||
string? cliente,
|
||||
string? linha,
|
||||
Guid vigenciaLineId)
|
||||
{
|
||||
return new Notification
|
||||
{
|
||||
Tipo = tipo,
|
||||
Titulo = titulo,
|
||||
Mensagem = mensagem,
|
||||
Data = DateTime.UtcNow,
|
||||
ReferenciaData = referenciaData,
|
||||
DiasParaVencer = diasParaVencer,
|
||||
Lida = false,
|
||||
DedupKey = BuildDedupKey(tipo, vigenciaLineId, referenciaData, diasParaVencer, usuario, cliente, linha),
|
||||
UserId = userId,
|
||||
Usuario = usuario,
|
||||
Cliente = cliente,
|
||||
Linha = linha,
|
||||
VigenciaLineId = vigenciaLineId
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildDedupKey(
|
||||
string tipo,
|
||||
Guid vigenciaLineId,
|
||||
DateTime referenciaData,
|
||||
int diasParaVencer,
|
||||
string? usuario,
|
||||
string? cliente,
|
||||
string? linha)
|
||||
{
|
||||
var parts = new[]
|
||||
{
|
||||
tipo.Trim().ToLowerInvariant(),
|
||||
vigenciaLineId.ToString(),
|
||||
referenciaData.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
diasParaVencer.ToString(CultureInfo.InvariantCulture),
|
||||
(usuario ?? string.Empty).Trim().ToLowerInvariant(),
|
||||
(cliente ?? string.Empty).Trim().ToLowerInvariant(),
|
||||
(linha ?? string.Empty).Trim().ToLowerInvariant()
|
||||
};
|
||||
|
||||
return string.Join('|', parts);
|
||||
}
|
||||
|
||||
private static string FormatLinha(string? linha)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(linha) ? string.Empty : $" {linha}";
|
||||
}
|
||||
}
|
||||
|
|
@ -4,5 +4,9 @@
|
|||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Notifications": {
|
||||
"CheckIntervalMinutes": 60,
|
||||
"ReminderDays": [30, 15, 7]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,9 @@
|
|||
"Issuer": "LineGestao",
|
||||
"Audience": "LineGestao",
|
||||
"ExpiresMinutes": 120
|
||||
},
|
||||
"Notifications": {
|
||||
"CheckIntervalMinutes": 60,
|
||||
"ReminderDays": [30, 15, 7]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue