Compare commits

...

3 Commits

Author SHA1 Message Date
Leon 3e6319566b feat: Implementação de aparelhos e anexos 2026-03-03 17:42:27 -03:00
Eduardo Lopes 355267b740 fix: ajustar tenant/cliente no cadastro de linhas e reserva 2026-03-03 14:45:39 -03:00
Eduardo 0d23967fea Feat: Deploy Alterações Logo 2026-03-03 13:10:20 -03:00
13 changed files with 1231 additions and 75 deletions

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
// ✅ tabela para espelhar a planilha (GERAL)
public DbSet<MobileLine> MobileLines => Set<MobileLine>();
public DbSet<Setor> Setores => Set<Setor>();
public DbSet<Aparelho> Aparelhos => Set<Aparelho>();
// ✅ tabela para espelhar a aba MUREG
public DbSet<MuregLine> MuregLines => Set<MuregLine>();
@ -87,6 +89,25 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
e.HasIndex(x => new { x.IsSystem, x.Ativo });
});
modelBuilder.Entity<Setor>(e =>
{
e.Property(x => x.Nome).HasMaxLength(160);
e.HasIndex(x => x.TenantId);
e.HasIndex(x => new { x.TenantId, x.Nome }).IsUnique();
});
modelBuilder.Entity<Aparelho>(e =>
{
e.Property(x => x.Nome).HasMaxLength(160);
e.Property(x => x.Cor).HasMaxLength(80);
e.Property(x => x.Imei).HasMaxLength(80);
e.Property(x => x.NotaFiscalArquivoPath).HasMaxLength(500);
e.Property(x => x.ReciboArquivoPath).HasMaxLength(500);
e.HasIndex(x => x.TenantId);
e.HasIndex(x => x.Imei);
e.HasIndex(x => new { x.TenantId, x.Nome, x.Cor });
});
// =========================
// ✅ USER (Identity)
// =========================
@ -104,13 +125,27 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
{
// Mantém UNIQUE por Linha por tenant (se Linha puder ser null no banco, Postgres aceita múltiplos nulls)
e.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
e.Property(x => x.CentroDeCustos).HasMaxLength(180);
// performance
e.HasIndex(x => x.Chip);
e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Usuario);
e.HasIndex(x => x.CentroDeCustos);
e.HasIndex(x => x.Skil);
e.HasIndex(x => x.Status);
e.HasIndex(x => x.SetorId);
e.HasIndex(x => x.AparelhoId);
e.HasOne(x => x.Setor)
.WithMany(x => x.MobileLines)
.HasForeignKey(x => x.SetorId)
.OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Aparelho)
.WithMany(x => x.MobileLines)
.HasForeignKey(x => x.AparelhoId)
.OnDelete(DeleteBehavior.SetNull);
});
// =========================
@ -336,6 +371,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
modelBuilder.Entity<MobileLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<MuregLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<Setor>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<Aparelho>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<BillingClient>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<UserData>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));
modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));

View File

@ -35,12 +35,8 @@ public static class SeedData
await db.Database.EnsureCreatedAsync();
}
if (!options.Enabled)
{
return;
}
var systemTenantId = SystemTenantConstants.SystemTenantId;
// Mantem o contrato de roles atualizado em todos os ambientes, inclusive
// quando o seed de usuario master estiver desabilitado.
var roles = AppRoles.All;
foreach (var role in roles)
{
@ -53,6 +49,12 @@ public static class SeedData
await MigrateLegacyRolesAsync(db, roleManager);
if (!options.Enabled)
{
return;
}
var systemTenantId = SystemTenantConstants.SystemTenantId;
var systemTenant = await db.Tenants.FirstOrDefaultAsync(t => t.Id == systemTenantId);
if (systemTenant == null)
{

View File

@ -12,6 +12,14 @@ namespace line_gestao_api.Dtos
public string? Chip { get; set; } // ICCID
public string? Cliente { get; set; } // Obrigatório na validação do Controller
public string? Usuario { get; set; }
public string? CentroDeCustos { get; set; }
public Guid? SetorId { get; set; }
public string? SetorNome { get; set; }
public Guid? AparelhoId { get; set; }
public string? AparelhoNome { get; set; }
public string? AparelhoCor { get; set; }
public string? AparelhoImei { get; set; }
public Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva
// ==========================
// Classificação e Status

View File

@ -9,6 +9,10 @@
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? CentroDeCustos { get; set; }
public string? SetorNome { get; set; }
public string? AparelhoNome { get; set; }
public string? AparelhoCor { get; set; }
public string? PlanoContrato { get; set; }
public string? Status { get; set; }
public string? Skil { get; set; }
@ -35,6 +39,15 @@
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? CentroDeCustos { get; set; }
public Guid? SetorId { get; set; }
public string? SetorNome { get; set; }
public Guid? AparelhoId { get; set; }
public string? AparelhoNome { get; set; }
public string? AparelhoCor { get; set; }
public string? AparelhoImei { get; set; }
public bool AparelhoNotaFiscalTemArquivo { get; set; }
public bool AparelhoReciboTemArquivo { get; set; }
public string? PlanoContrato { get; set; }
public decimal? FranquiaVivo { get; set; }
@ -78,6 +91,13 @@
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? CentroDeCustos { get; set; }
public Guid? SetorId { get; set; }
public string? SetorNome { get; set; }
public Guid? AparelhoId { get; set; }
public string? AparelhoNome { get; set; }
public string? AparelhoCor { get; set; }
public string? AparelhoImei { get; set; }
public string? PlanoContrato { get; set; }
public decimal? FranquiaVivo { get; set; }

View File

@ -0,0 +1,114 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260303120000_AddSetoresAparelhosAndMobileLineCostCenter")]
public partial class AddSetoresAparelhosAndMobileLineCostCenter : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "Setores" (
"Id" uuid NOT NULL,
"TenantId" uuid NOT NULL,
"Nome" character varying(160) NOT NULL,
"CreatedAt" timestamp with time zone NOT NULL,
"UpdatedAt" timestamp with time zone NOT NULL,
CONSTRAINT "PK_Setores" PRIMARY KEY ("Id")
);
""");
migrationBuilder.Sql("""
CREATE TABLE IF NOT EXISTS "Aparelhos" (
"Id" uuid NOT NULL,
"TenantId" uuid NOT NULL,
"Nome" character varying(160) NULL,
"Cor" character varying(80) NULL,
"Imei" character varying(80) NULL,
"NotaFiscalArquivoPath" character varying(500) NULL,
"ReciboArquivoPath" character varying(500) NULL,
"CreatedAt" timestamp with time zone NOT NULL,
"UpdatedAt" timestamp with time zone NOT NULL,
CONSTRAINT "PK_Aparelhos" PRIMARY KEY ("Id")
);
""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "CentroDeCustos" character varying(180) NULL;""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "SetorId" uuid NULL;""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" ADD COLUMN IF NOT EXISTS "AparelhoId" uuid NULL;""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Setores_TenantId" ON "Setores" ("TenantId");""");
migrationBuilder.Sql("""CREATE UNIQUE INDEX IF NOT EXISTS "IX_Setores_TenantId_Nome" ON "Setores" ("TenantId", "Nome");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_TenantId" ON "Aparelhos" ("TenantId");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_Imei" ON "Aparelhos" ("Imei");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_Aparelhos_TenantId_Nome_Cor" ON "Aparelhos" ("TenantId", "Nome", "Cor");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_CentroDeCustos" ON "MobileLines" ("CentroDeCustos");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_SetorId" ON "MobileLines" ("SetorId");""");
migrationBuilder.Sql("""CREATE INDEX IF NOT EXISTS "IX_MobileLines_AparelhoId" ON "MobileLines" ("AparelhoId");""");
migrationBuilder.Sql("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints
WHERE constraint_name = 'FK_MobileLines_Setores_SetorId'
AND table_name = 'MobileLines'
) THEN
ALTER TABLE "MobileLines"
ADD CONSTRAINT "FK_MobileLines_Setores_SetorId"
FOREIGN KEY ("SetorId") REFERENCES "Setores" ("Id")
ON DELETE SET NULL;
END IF;
END
$$;
""");
migrationBuilder.Sql("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints
WHERE constraint_name = 'FK_MobileLines_Aparelhos_AparelhoId'
AND table_name = 'MobileLines'
) THEN
ALTER TABLE "MobileLines"
ADD CONSTRAINT "FK_MobileLines_Aparelhos_AparelhoId"
FOREIGN KEY ("AparelhoId") REFERENCES "Aparelhos" ("Id")
ON DELETE SET NULL;
END IF;
END
$$;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP CONSTRAINT IF EXISTS "FK_MobileLines_Setores_SetorId";""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP CONSTRAINT IF EXISTS "FK_MobileLines_Aparelhos_AparelhoId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_AparelhoId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_SetorId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_MobileLines_CentroDeCustos";""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "AparelhoId";""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "SetorId";""");
migrationBuilder.Sql("""ALTER TABLE "MobileLines" DROP COLUMN IF EXISTS "CentroDeCustos";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_TenantId_Nome_Cor";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_Imei";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Aparelhos_TenantId";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Setores_TenantId_Nome";""");
migrationBuilder.Sql("""DROP INDEX IF EXISTS "IX_Setores_TenantId";""");
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Aparelhos";""");
migrationBuilder.Sql("""DROP TABLE IF EXISTS "Setores";""");
}
}
}

View File

@ -0,0 +1,64 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using line_gestao_api.Data;
#nullable disable
namespace line_gestao_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260303193000_FixAparelhosArquivoPathColumns")]
public partial class FixAparelhosArquivoPathColumns : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
ALTER TABLE "Aparelhos"
ADD COLUMN IF NOT EXISTS "NotaFiscalArquivoPath" character varying(500) NULL;
""");
migrationBuilder.Sql("""
ALTER TABLE "Aparelhos"
ADD COLUMN IF NOT EXISTS "ReciboArquivoPath" character varying(500) NULL;
""");
// Backfill seguro para bancos que já tinham os campos antigos de URL/nome.
migrationBuilder.Sql("""
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Aparelhos'
AND column_name = 'NotaFiscalAnexoUrl'
) THEN
UPDATE "Aparelhos"
SET "NotaFiscalArquivoPath" = COALESCE("NotaFiscalArquivoPath", "NotaFiscalAnexoUrl")
WHERE "NotaFiscalAnexoUrl" IS NOT NULL
AND "NotaFiscalAnexoUrl" <> '';
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Aparelhos'
AND column_name = 'ReciboAnexoUrl'
) THEN
UPDATE "Aparelhos"
SET "ReciboArquivoPath" = COALESCE("ReciboArquivoPath", "ReciboAnexoUrl")
WHERE "ReciboAnexoUrl" IS NOT NULL
AND "ReciboAnexoUrl" <> '';
END IF;
END
$$;
""");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""ALTER TABLE "Aparelhos" DROP COLUMN IF EXISTS "ReciboArquivoPath";""");
migrationBuilder.Sql("""ALTER TABLE "Aparelhos" DROP COLUMN IF EXISTS "NotaFiscalArquivoPath";""");
}
}
}

View File

@ -330,6 +330,52 @@ namespace line_gestao_api.Migrations
b.ToTable("AuditLogs");
});
modelBuilder.Entity("line_gestao_api.Models.Aparelho", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Cor")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Imei")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
b.Property<string>("Nome")
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("NotaFiscalArquivoPath")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("ReciboArquivoPath")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Imei");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Nome", "Cor");
b.ToTable("Aparelhos");
});
modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
{
b.Property<Guid>("Id")
@ -624,10 +670,17 @@ namespace line_gestao_api.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid?>("AparelhoId")
.HasColumnType("uuid");
b.Property<string>("Cedente")
.HasMaxLength(150)
.HasColumnType("character varying(150)");
b.Property<string>("CentroDeCustos")
.HasMaxLength(180)
.HasColumnType("character varying(180)");
b.Property<string>("Chip")
.HasMaxLength(40)
.HasColumnType("character varying(40)");
@ -691,6 +744,9 @@ namespace line_gestao_api.Migrations
b.Property<decimal?>("Skeelo")
.HasColumnType("numeric");
b.Property<Guid?>("SetorId")
.HasColumnType("uuid");
b.Property<string>("Skil")
.HasMaxLength(80)
.HasColumnType("character varying(80)");
@ -744,10 +800,16 @@ namespace line_gestao_api.Migrations
b.HasKey("Id");
b.HasIndex("AparelhoId");
b.HasIndex("Chip");
b.HasIndex("CentroDeCustos");
b.HasIndex("Cliente");
b.HasIndex("SetorId");
b.HasIndex("Skil");
b.HasIndex("Status");
@ -1370,6 +1432,36 @@ namespace line_gestao_api.Migrations
b.ToTable("ResumoVivoLineTotals");
});
modelBuilder.Entity("line_gestao_api.Models.Setor", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Nome")
.IsRequired()
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<Guid>("TenantId")
.HasColumnType("uuid");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("TenantId");
b.HasIndex("TenantId", "Nome")
.IsUnique();
b.ToTable("Setores");
});
modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{
b.Property<Guid>("Id")
@ -1665,6 +1757,23 @@ namespace line_gestao_api.Migrations
.IsRequired();
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{
b.HasOne("line_gestao_api.Models.Aparelho", "Aparelho")
.WithMany("MobileLines")
.HasForeignKey("AparelhoId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("line_gestao_api.Models.Setor", "Setor")
.WithMany("MobileLines")
.HasForeignKey("SetorId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Aparelho");
b.Navigation("Setor");
});
modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
@ -1715,6 +1824,11 @@ namespace line_gestao_api.Migrations
b.Navigation("ParcelamentoLine");
});
modelBuilder.Entity("line_gestao_api.Models.Aparelho", b =>
{
b.Navigation("MobileLines");
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{
b.Navigation("Muregs");
@ -1729,6 +1843,11 @@ namespace line_gestao_api.Migrations
{
b.Navigation("MonthValues");
});
modelBuilder.Entity("line_gestao_api.Models.Setor", b =>
{
b.Navigation("MobileLines");
});
#pragma warning restore 612, 618
}
}

30
Models/Aparelho.cs Normal file
View File

@ -0,0 +1,30 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models;
public class Aparelho : ITenantEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
[MaxLength(160)]
public string? Nome { get; set; }
[MaxLength(80)]
public string? Cor { get; set; }
[MaxLength(80)]
public string? Imei { get; set; }
[MaxLength(500)]
public string? NotaFiscalArquivoPath { get; set; }
[MaxLength(500)]
public string? ReciboArquivoPath { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<MobileLine> MobileLines { get; set; } = new List<MobileLine>();
}

View File

@ -20,6 +20,15 @@ namespace line_gestao_api.Models
public string? Cliente { get; set; }
[MaxLength(200)]
public string? Usuario { get; set; }
[MaxLength(180)]
public string? CentroDeCustos { get; set; }
public Guid? SetorId { get; set; }
public Setor? Setor { get; set; }
public Guid? AparelhoId { get; set; }
public Aparelho? Aparelho { get; set; }
[MaxLength(200)]
public string? PlanoContrato { get; set; }

18
Models/Setor.cs Normal file
View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models;
public class Setor : ITenantEntity
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid TenantId { get; set; }
[MaxLength(160)]
public string Nome { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public ICollection<MobileLine> MobileLines { get; set; } = new List<MobileLine>();
}