Ajustes API: import excel, CORS/Swagger e endpoints lines

This commit is contained in:
Eduardo 2025-12-17 18:03:07 -03:00
parent a16358a630
commit b4a5525578
11 changed files with 1035 additions and 6 deletions

View File

@ -0,0 +1,478 @@
using ClosedXML.Excel;
using line_gestao_api.Data;
using line_gestao_api.Dtos;
using line_gestao_api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Globalization;
using System.Text;
namespace line_gestao_api.Controllers
{
[ApiController]
[Route("api/[controller]")]
//[Authorize]
public class LinesController : ControllerBase
{
private readonly AppDbContext _db;
public LinesController(AppDbContext db)
{
_db = db;
}
// ✅ DTO do form (pra Swagger entender multipart/form-data)
public class ImportExcelForm
{
public IFormFile File { get; set; } = default!;
}
[HttpGet]
public async Task<ActionResult<PagedResult<MobileLineListDto>>> GetAll(
[FromQuery] string? search,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? sortBy = "item",
[FromQuery] string? sortDir = "asc")
{
page = page < 1 ? 1 : page;
pageSize = pageSize < 1 ? 20 : pageSize;
var q = _db.MobileLines.AsNoTracking();
if (!string.IsNullOrWhiteSpace(search))
{
var s = search.Trim();
q = q.Where(x =>
EF.Functions.ILike(x.Linha ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Chip ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Cliente ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Usuario ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Conta ?? "", $"%{s}%") ||
EF.Functions.ILike(x.Status ?? "", $"%{s}%"));
}
var total = await q.CountAsync();
// ===== ORDENAÇÃO COMPLETA =====
var sb = (sortBy ?? "item").Trim().ToLowerInvariant();
var desc = string.Equals((sortDir ?? "asc").Trim(), "desc", StringComparison.OrdinalIgnoreCase);
// sinônimos (pra não quebrar se vier diferente do front)
if (sb == "plano") sb = "planocontrato";
if (sb == "contrato") sb = "vencconta";
q = sb switch
{
"conta" => desc ? q.OrderByDescending(x => x.Conta ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Conta ?? "").ThenBy(x => x.Item),
"linha" => desc ? q.OrderByDescending(x => x.Linha ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Linha ?? "").ThenBy(x => x.Item),
"chip" => desc ? q.OrderByDescending(x => x.Chip ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Chip ?? "").ThenBy(x => x.Item),
"cliente" => desc ? q.OrderByDescending(x => x.Cliente ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Cliente ?? "").ThenBy(x => x.Item),
"usuario" => desc ? q.OrderByDescending(x => x.Usuario ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Usuario ?? "").ThenBy(x => x.Item),
"planocontrato" => desc ? q.OrderByDescending(x => x.PlanoContrato ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.PlanoContrato ?? "").ThenBy(x => x.Item),
"vencconta" => desc ? q.OrderByDescending(x => x.VencConta ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.VencConta ?? "").ThenBy(x => x.Item),
"status" => desc ? q.OrderByDescending(x => x.Status ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Status ?? "").ThenBy(x => x.Item),
"skil" => desc ? q.OrderByDescending(x => x.Skil ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Skil ?? "").ThenBy(x => x.Item),
"modalidade" => desc ? q.OrderByDescending(x => x.Modalidade ?? "").ThenBy(x => x.Item)
: q.OrderBy(x => x.Modalidade ?? "").ThenBy(x => x.Item),
_ => desc ? q.OrderByDescending(x => x.Item)
: q.OrderBy(x => x.Item)
};
var items = await q
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(x => new MobileLineListDto
{
Id = x.Id,
Item = x.Item,
Conta = x.Conta,
Linha = x.Linha,
Chip = x.Chip,
Cliente = x.Cliente,
Usuario = x.Usuario,
PlanoContrato = x.PlanoContrato,
Status = x.Status,
Skil = x.Skil,
Modalidade = x.Modalidade,
VencConta = x.VencConta
})
.ToListAsync();
return Ok(new PagedResult<MobileLineListDto>
{
Page = page,
PageSize = pageSize,
Total = total,
Items = items
});
}
[HttpGet("{id:guid}")]
public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id)
{
var x = await _db.MobileLines.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound();
return Ok(ToDetailDto(x));
}
[HttpPut("{id:guid}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
{
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound();
x.Item = req.Item;
x.Conta = req.Conta;
x.Linha = req.Linha;
x.Chip = req.Chip;
x.Cliente = req.Cliente;
x.Usuario = req.Usuario;
x.PlanoContrato = req.PlanoContrato;
x.FranquiaVivo = req.FranquiaVivo;
x.ValorPlanoVivo = req.ValorPlanoVivo;
x.GestaoVozDados = req.GestaoVozDados;
x.Skeelo = req.Skeelo;
x.VivoNewsPlus = req.VivoNewsPlus;
x.VivoTravelMundo = req.VivoTravelMundo;
x.VivoGestaoDispositivo = req.VivoGestaoDispositivo;
x.ValorContratoVivo = req.ValorContratoVivo;
x.FranquiaLine = req.FranquiaLine;
x.FranquiaGestao = req.FranquiaGestao;
x.LocacaoAp = req.LocacaoAp;
x.ValorContratoLine = req.ValorContratoLine;
x.Desconto = req.Desconto;
x.Lucro = req.Lucro;
x.Status = req.Status;
x.DataBloqueio = ToUtc(req.DataBloqueio);
x.Skil = req.Skil;
x.Modalidade = req.Modalidade;
x.Cedente = req.Cedente;
x.Solicitante = req.Solicitante;
x.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
x.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
x.VencConta = req.VencConta;
// regra RESERVA
ApplyReservaRule(x);
x.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id:guid}")]
public async Task<IActionResult> Delete(Guid id)
{
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound();
_db.MobileLines.Remove(x);
await _db.SaveChangesAsync();
return NoContent();
}
[HttpPost("import-excel")]
[Consumes("multipart/form-data")]
[RequestSizeLimit(50_000_000)]
public async Task<ActionResult<ImportResultDto>> ImportExcel([FromForm] ImportExcelForm form)
{
var file = form.File;
if (file == null || file.Length == 0)
return BadRequest("Arquivo inválido.");
using var stream = file.OpenReadStream();
using var wb = new XLWorkbook(stream);
var ws = wb.Worksheets.FirstOrDefault(w => w.Name.Trim().Equals("GERAL", StringComparison.OrdinalIgnoreCase));
if (ws == null)
return BadRequest("Aba 'GERAL' não encontrada.");
// acha a linha do cabeçalho (onde existe ITÉM)
var headerRow = ws.RowsUsed().FirstOrDefault(r =>
r.CellsUsed().Any(c => NormalizeHeader(c.GetString()) == "ITEM"));
if (headerRow == null)
return BadRequest("Cabeçalho da planilha (linha com 'ITÉM') não encontrado.");
// mapa header -> coluna
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
foreach (var cell in headerRow.CellsUsed())
{
var key = NormalizeHeader(cell.GetString());
if (!string.IsNullOrWhiteSpace(key) && !map.ContainsKey(key))
map[key] = cell.Address.ColumnNumber;
}
int colItem = GetCol(map, "ITEM");
if (colItem == 0) return BadRequest("Coluna 'ITÉM' não encontrada.");
var startRow = headerRow.RowNumber() + 1;
// REPLACE: apaga tudo e reimporta (pra espelhar 100% o Excel)
await _db.MobileLines.ExecuteDeleteAsync();
var imported = 0;
var buffer = new List<MobileLine>(600);
for (int r = startRow; r <= ws.LastRowUsed().RowNumber(); r++)
{
var itemStr = GetCellString(ws, r, colItem);
if (string.IsNullOrWhiteSpace(itemStr)) break;
var entity = new MobileLine
{
Item = TryInt(itemStr),
Conta = GetCellByHeader(ws, r, map, "CONTA"),
Linha = OnlyDigits(GetCellByHeader(ws, r, map, "LINHA")),
Chip = OnlyDigits(GetCellByHeader(ws, r, map, "CHIP")),
Cliente = GetCellByHeader(ws, r, map, "CLIENTE"),
Usuario = GetCellByHeader(ws, r, map, "USUARIO"),
PlanoContrato = GetCellByHeader(ws, r, map, "PLANO CONTRATO"),
FranquiaVivo = TryDecimal(GetCellByHeader(ws, r, map, "FRAQUIA")),
ValorPlanoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR DO PLANO R$")),
GestaoVozDados = TryDecimal(GetCellByHeader(ws, r, map, "GESTAO VOZ E DADOS R$")),
Skeelo = TryDecimal(GetCellByHeader(ws, r, map, "SKEELO")),
VivoNewsPlus = TryDecimal(GetCellByHeader(ws, r, map, "VIVO NEWS PLUS")),
VivoTravelMundo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO TRAVEL MUNDO")),
VivoGestaoDispositivo = TryDecimal(GetCellByHeader(ws, r, map, "VIVO GESTAO DISPOSITIVO")),
ValorContratoVivo = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO VIVO")),
FranquiaLine = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA LINE")),
FranquiaGestao = TryDecimal(GetCellByHeader(ws, r, map, "FRANQUIA GESTAO")),
LocacaoAp = TryDecimal(GetCellByHeader(ws, r, map, "LOCACAO AP.")),
ValorContratoLine = TryDecimal(GetCellByHeader(ws, r, map, "VALOR CONTRATO LINE")),
Desconto = TryDecimal(GetCellByHeader(ws, r, map, "DESCONTO")),
Lucro = TryDecimal(GetCellByHeader(ws, r, map, "LUCRO")),
Status = GetCellByHeader(ws, r, map, "STATUS"),
DataBloqueio = TryDate(ws, r, map, "DATA DO BLOQUEIO"),
Skil = GetCellByHeader(ws, r, map, "SKIL"),
Modalidade = GetCellByHeader(ws, r, map, "MODALIDADE"),
Cedente = GetCellByHeader(ws, r, map, "CEDENTE"),
Solicitante = GetCellByHeader(ws, r, map, "SOLICITANTE"),
DataEntregaOpera = TryDate(ws, r, map, "DATA DA ENTREGA OPERA."),
DataEntregaCliente = TryDate(ws, r, map, "DATA DA ENTREGA CLIENTE"),
VencConta = GetCellByHeader(ws, r, map, "VENC. DA CONTA"),
};
ApplyReservaRule(entity);
buffer.Add(entity);
imported++;
if (buffer.Count >= 500)
{
await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
buffer.Clear();
}
}
if (buffer.Count > 0)
{
await _db.MobileLines.AddRangeAsync(buffer);
await _db.SaveChangesAsync();
}
return Ok(new ImportResultDto { Imported = imported });
}
// ================= helpers =================
private static DateTime? ToUtc(DateTime? dt)
{
if (dt == null) return null;
var v = dt.Value;
return v.Kind switch
{
DateTimeKind.Utc => v,
DateTimeKind.Local => v.ToUniversalTime(),
_ => DateTime.SpecifyKind(v, DateTimeKind.Utc) // Unspecified -> UTC (sem shift)
};
}
private static MobileLineDetailDto ToDetailDto(MobileLine x) => new()
{
Id = x.Id,
Item = x.Item,
Conta = x.Conta,
Linha = x.Linha,
Chip = x.Chip,
Cliente = x.Cliente,
Usuario = x.Usuario,
PlanoContrato = x.PlanoContrato,
FranquiaVivo = x.FranquiaVivo,
ValorPlanoVivo = x.ValorPlanoVivo,
GestaoVozDados = x.GestaoVozDados,
Skeelo = x.Skeelo,
VivoNewsPlus = x.VivoNewsPlus,
VivoTravelMundo = x.VivoTravelMundo,
VivoGestaoDispositivo = x.VivoGestaoDispositivo,
ValorContratoVivo = x.ValorContratoVivo,
FranquiaLine = x.FranquiaLine,
FranquiaGestao = x.FranquiaGestao,
LocacaoAp = x.LocacaoAp,
ValorContratoLine = x.ValorContratoLine,
Desconto = x.Desconto,
Lucro = x.Lucro,
Status = x.Status,
DataBloqueio = x.DataBloqueio,
Skil = x.Skil,
Modalidade = x.Modalidade,
Cedente = x.Cedente,
Solicitante = x.Solicitante,
DataEntregaOpera = x.DataEntregaOpera,
DataEntregaCliente = x.DataEntregaCliente,
VencConta = x.VencConta
};
private static void ApplyReservaRule(MobileLine x)
{
var cliente = (x.Cliente ?? "").Trim();
var usuario = (x.Usuario ?? "").Trim();
if (cliente.Equals("RESERVA", StringComparison.OrdinalIgnoreCase) ||
usuario.Equals("RESERVA", StringComparison.OrdinalIgnoreCase))
{
x.Cliente = "RESERVA";
x.Usuario = "RESERVA";
x.Skil = "RESERVA";
}
}
private static int GetCol(Dictionary<string, int> map, string name)
=> map.TryGetValue(NormalizeHeader(name), out var c) ? c : 0;
private static string GetCellByHeader(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
{
var key = NormalizeHeader(header);
if (!map.TryGetValue(key, out var col)) return "";
return GetCellString(ws, row, col);
}
private static string GetCellString(IXLWorksheet ws, int row, int col)
{
var cell = ws.Cell(row, col);
if (cell == null) return "";
var v = cell.GetValue<string>() ?? "";
return v.Trim();
}
private static DateTime? TryDate(IXLWorksheet ws, int row, Dictionary<string, int> map, string header)
{
var key = NormalizeHeader(header);
if (!map.TryGetValue(key, out var col)) return null;
var cell = ws.Cell(row, col);
if (cell.DataType == XLDataType.DateTime)
return ToUtc(cell.GetDateTime());
var s = cell.GetValue<string>()?.Trim();
if (string.IsNullOrWhiteSpace(s)) return null;
if (DateTime.TryParse(s, new CultureInfo("pt-BR"), DateTimeStyles.None, out var d))
return ToUtc(d);
if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out d))
return ToUtc(d);
return null;
}
private static decimal? TryDecimal(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return null;
// remove "R$", espaços etc.
s = s.Replace("R$", "", StringComparison.OrdinalIgnoreCase).Trim();
if (decimal.TryParse(s, NumberStyles.Any, new CultureInfo("pt-BR"), out var d))
return d;
if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out d))
return d;
return null;
}
private static int TryInt(string s)
=> int.TryParse(OnlyDigits(s), out var n) ? n : 0;
private static string OnlyDigits(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return "";
var sb = new StringBuilder();
foreach (var ch in s)
if (char.IsDigit(ch)) sb.Append(ch);
return sb.ToString();
}
private static string NormalizeHeader(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return "";
s = s.Trim().ToUpperInvariant();
// remove acentos
var formD = s.Normalize(NormalizationForm.FormD);
var sb = new StringBuilder();
foreach (var ch in formD)
if (System.Globalization.CharUnicodeInfo.GetUnicodeCategory(ch) != System.Globalization.UnicodeCategory.NonSpacingMark)
sb.Append(ch);
s = sb.ToString().Normalize(NormalizationForm.FormC);
// normalizações pra casar com a planilha
s = s.Replace("ITÉM", "ITEM")
.Replace("USUÁRIO", "USUARIO")
.Replace("GESTÃO", "GESTAO")
.Replace("LOCAÇÃO", "LOCACAO");
// remove espaços duplicados
s = string.Join(" ", s.Split(' ', StringSplitOptions.RemoveEmptyEntries));
return s;
}
}
}

View File

@ -9,12 +9,21 @@ public class AppDbContext : DbContext
public DbSet<User> Users => Set<User>();
// ✅ NOVO: tabela para espelhar a planilha (GERAL)
public DbSet<MobileLine> MobileLines => Set<MobileLine>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// ✅ MANTIDO: índice único do User (não mexi em nada aqui)
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
// ✅ NOVO: índice único para evitar duplicar a mesma linha (telefone)
modelBuilder.Entity<MobileLine>()
.HasIndex(x => x.Linha)
.IsUnique();
}
}

9
Dtos/ImportExcelForm.cs Normal file
View File

@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Http;
namespace line_gestao_api.Dtos
{
public class ImportExcelForm
{
public IFormFile File { get; set; } = default!;
}
}

67
Dtos/MobileLineDtos.cs Normal file
View File

@ -0,0 +1,67 @@
namespace line_gestao_api.Dtos
{
public class MobileLineListDto
{
public Guid Id { get; set; }
public int Item { get; set; }
public string? Conta { get; set; }
public string? Linha { get; set; }
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? PlanoContrato { get; set; }
public string? Status { get; set; }
public string? Skil { get; set; }
public string? Modalidade { get; set; }
public string? VencConta { get; set; }
}
public class MobileLineDetailDto
{
public Guid Id { get; set; }
public int Item { get; set; }
public string? Conta { get; set; }
public string? Linha { get; set; }
public string? Chip { get; set; }
public string? Cliente { get; set; }
public string? Usuario { get; set; }
public string? PlanoContrato { get; set; }
public decimal? FranquiaVivo { get; set; }
public decimal? ValorPlanoVivo { get; set; }
public decimal? GestaoVozDados { get; set; }
public decimal? Skeelo { get; set; }
public decimal? VivoNewsPlus { get; set; }
public decimal? VivoTravelMundo { get; set; }
public decimal? VivoGestaoDispositivo { get; set; }
public decimal? ValorContratoVivo { get; set; }
public decimal? FranquiaLine { get; set; }
public decimal? FranquiaGestao { get; set; }
public decimal? LocacaoAp { get; set; }
public decimal? ValorContratoLine { get; set; }
public decimal? Desconto { get; set; }
public decimal? Lucro { get; set; }
public string? Status { get; set; }
public DateTime? DataBloqueio { get; set; }
public string? Skil { get; set; }
public string? Modalidade { get; set; }
public string? Cedente { get; set; }
public string? Solicitante { get; set; }
public DateTime? DataEntregaOpera { get; set; }
public DateTime? DataEntregaCliente { get; set; }
public string? VencConta { get; set; }
}
public class UpdateMobileLineRequest : MobileLineDetailDto
{
// reaproveita os campos; Id vem na rota
}
public class ImportResultDto
{
public int Imported { get; set; }
}
}

10
Dtos/PagedResult.cs Normal file
View File

@ -0,0 +1,10 @@
namespace line_gestao_api.Dtos
{
public class PagedResult<T>
{
public int Page { get; set; }
public int PageSize { get; set; }
public int Total { get; set; }
public List<T> Items { get; set; } = new();
}
}

View File

@ -0,0 +1,188 @@
// <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("20251217170445_AddMobileLines")]
partial class AddMobileLines
{
/// <inheritdoc />
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.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("Linha")
.IsUnique();
b.ToTable("MobileLines");
});
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");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace line_gestao_api.Migrations
{
/// <inheritdoc />
public partial class AddMobileLines : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MobileLines",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Item = table.Column<int>(type: "integer", nullable: false),
Conta = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: true),
Linha = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: true),
Chip = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true),
Cliente = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Usuario = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
PlanoContrato = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
FranquiaVivo = table.Column<decimal>(type: "numeric", nullable: true),
ValorPlanoVivo = table.Column<decimal>(type: "numeric", nullable: true),
GestaoVozDados = table.Column<decimal>(type: "numeric", nullable: true),
Skeelo = table.Column<decimal>(type: "numeric", nullable: true),
VivoNewsPlus = table.Column<decimal>(type: "numeric", nullable: true),
VivoTravelMundo = table.Column<decimal>(type: "numeric", nullable: true),
VivoGestaoDispositivo = table.Column<decimal>(type: "numeric", nullable: true),
ValorContratoVivo = table.Column<decimal>(type: "numeric", nullable: true),
FranquiaLine = table.Column<decimal>(type: "numeric", nullable: true),
FranquiaGestao = table.Column<decimal>(type: "numeric", nullable: true),
LocacaoAp = table.Column<decimal>(type: "numeric", nullable: true),
ValorContratoLine = table.Column<decimal>(type: "numeric", nullable: true),
Desconto = table.Column<decimal>(type: "numeric", nullable: true),
Lucro = table.Column<decimal>(type: "numeric", nullable: true),
Status = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: true),
DataBloqueio = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Skil = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: true),
Modalidade = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: true),
Cedente = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true),
Solicitante = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: true),
DataEntregaOpera = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
DataEntregaCliente = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
VencConta = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MobileLines", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_MobileLines_Linha",
table: "MobileLines",
column: "Linha",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MobileLines");
}
}
}

View File

@ -22,6 +22,128 @@ namespace line_gestao_api.Migrations
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
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("Linha")
.IsUnique();
b.ToTable("MobileLines");
});
modelBuilder.Entity("line_gestao_api.Models.User", b =>
{
b.Property<Guid>("Id")

67
Models/MobileLine.cs Normal file
View File

@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
namespace line_gestao_api.Models
{
public class MobileLine
{
public Guid Id { get; set; } = Guid.NewGuid();
// ===== Planilha (GERAL) =====
public int Item { get; set; } // ITÉM
[MaxLength(80)]
public string? Conta { get; set; } // CONTA
[MaxLength(30)]
public string? Linha { get; set; } // LINHA (telefone)
[MaxLength(40)]
public string? Chip { get; set; } // CHIP
[MaxLength(200)]
public string? Cliente { get; set; } // CLIENTE
[MaxLength(200)]
public string? Usuario { get; set; } // USUÁRIO
[MaxLength(200)]
public string? PlanoContrato { get; set; } // PLANO CONTRATO
// ===== Valores Vivo (ROXO no modal do front) =====
public decimal? FranquiaVivo { get; set; } // FRAQUIA
public decimal? ValorPlanoVivo { get; set; } // VALOR DO PLANO R$
public decimal? GestaoVozDados { get; set; } // GESTÃO VOZ E DADOS R$
public decimal? Skeelo { get; set; } // SKEELO
public decimal? VivoNewsPlus { get; set; } // VIVO NEWS PLUS
public decimal? VivoTravelMundo { get; set; } // VIVO TRAVEL MUNDO
public decimal? VivoGestaoDispositivo { get; set; } // VIVO GESTÃO DISPOSITIVO
public decimal? ValorContratoVivo { get; set; } // VALOR CONTRATO VIVO
// ===== Valores Line Móvel (paleta do sistema no modal) =====
public decimal? FranquiaLine { get; set; } // FRANQUIA LINE
public decimal? FranquiaGestao { get; set; } // FRANQUIA GESTÃO
public decimal? LocacaoAp { get; set; } // LOCAÇÃO AP.
public decimal? ValorContratoLine { get; set; } // VALOR CONTRATO LINE
public decimal? Desconto { get; set; } // DESCONTO
public decimal? Lucro { get; set; } // LUCRO
[MaxLength(80)]
public string? Status { get; set; } // STATUS
public DateTime? DataBloqueio { get; set; } // DATA DO BLOQUEIO
[MaxLength(80)]
public string? Skil { get; set; } // SKIL
[MaxLength(80)]
public string? Modalidade { get; set; } // MODALIDADE
[MaxLength(150)]
public string? Cedente { get; set; } // CEDENTE
[MaxLength(150)]
public string? Solicitante { get; set; } // SOLICITANTE
public DateTime? DataEntregaOpera { get; set; } // DATA DA ENTREGA OPERA.
public DateTime? DataEntregaCliente { get; set; } // DATA DA ENTREGA CLIENTE
[MaxLength(50)]
public string? VencConta { get; set; } // VENC. DA CONTA
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}

View File

@ -1,6 +1,7 @@
using System.Text;
using System.Text;
using line_gestao_api.Data;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
@ -8,7 +9,13 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ? CORS (Angular)
// ✅ Upload (Excel / multipart) - seguro e não quebra nada
builder.Services.Configure<FormOptions>(o =>
{
o.MultipartBodyLengthLimit = 50_000_000; // 50MB (mesmo do seu endpoint)
});
// ✅ CORS (Angular)
builder.Services.AddCors(options =>
{
options.AddPolicy("Front", p =>
@ -18,16 +25,16 @@ builder.Services.AddCors(options =>
);
});
// EF Core (PostgreSQL)
// EF Core (PostgreSQL)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
);
// Swagger
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// JWT
// JWT
var jwtKey = builder.Configuration["Jwt:Key"]!;
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];
@ -60,7 +67,7 @@ if (app.Environment.IsDevelopment())
app.UseHttpsRedirection();
// ? CORS precisa vir antes de Auth/Authorization
// CORS precisa vir antes de Auth/Authorization
app.UseCors("Front");
app.UseAuthentication();

View File

@ -20,6 +20,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ClosedXML" Version="0.105.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />