feat: Implementação de aparelhos e anexos

This commit is contained in:
Leon 2026-03-03 17:42:27 -03:00
parent 355267b740
commit 3e6319566b
12 changed files with 970 additions and 14 deletions

View File

@ -4,13 +4,16 @@ using line_gestao_api.Dtos;
using line_gestao_api.Models; using line_gestao_api.Models;
using line_gestao_api.Services; using line_gestao_api.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Security.Claims; using System.Security.Claims;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
@ -28,6 +31,8 @@ namespace line_gestao_api.Controllers
private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService; private readonly IVigenciaNotificationSyncService _vigenciaNotificationSyncService;
private readonly ParcelamentosImportService _parcelamentosImportService; private readonly ParcelamentosImportService _parcelamentosImportService;
private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService; private readonly SpreadsheetImportAuditService _spreadsheetImportAuditService;
private readonly string _aparelhoAttachmentsRootPath;
private static readonly FileExtensionContentTypeProvider FileContentTypeProvider = new();
private static readonly List<AccountCompanyDto> AccountCompanies = new() private static readonly List<AccountCompanyDto> AccountCompanies = new()
{ {
new AccountCompanyDto new AccountCompanyDto
@ -57,13 +62,15 @@ namespace line_gestao_api.Controllers
ITenantProvider tenantProvider, ITenantProvider tenantProvider,
IVigenciaNotificationSyncService vigenciaNotificationSyncService, IVigenciaNotificationSyncService vigenciaNotificationSyncService,
ParcelamentosImportService parcelamentosImportService, ParcelamentosImportService parcelamentosImportService,
SpreadsheetImportAuditService spreadsheetImportAuditService) SpreadsheetImportAuditService spreadsheetImportAuditService,
IWebHostEnvironment webHostEnvironment)
{ {
_db = db; _db = db;
_tenantProvider = tenantProvider; _tenantProvider = tenantProvider;
_vigenciaNotificationSyncService = vigenciaNotificationSyncService; _vigenciaNotificationSyncService = vigenciaNotificationSyncService;
_parcelamentosImportService = parcelamentosImportService; _parcelamentosImportService = parcelamentosImportService;
_spreadsheetImportAuditService = spreadsheetImportAuditService; _spreadsheetImportAuditService = spreadsheetImportAuditService;
_aparelhoAttachmentsRootPath = Path.Combine(webHostEnvironment.ContentRootPath, "uploads", "aparelhos");
} }
public class ImportExcelForm public class ImportExcelForm
@ -71,6 +78,12 @@ namespace line_gestao_api.Controllers
public IFormFile File { get; set; } = default!; public IFormFile File { get; set; } = default!;
} }
public class UploadAparelhoAnexosForm
{
public IFormFile? NotaFiscal { get; set; }
public IFormFile? Recibo { get; set; }
}
// ========================================================== // ==========================================================
// ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE // ✅ 1. ENDPOINT: AGRUPAR POR CLIENTE
// ========================================================== // ==========================================================
@ -523,6 +536,10 @@ namespace line_gestao_api.Controllers
line.Chip, line.Chip,
Cliente = clienteExibicao, Cliente = clienteExibicao,
line.Usuario, line.Usuario,
line.CentroDeCustos,
SetorNome = line.Setor != null ? line.Setor.Nome : null,
AparelhoNome = line.Aparelho != null ? line.Aparelho.Nome : null,
AparelhoCor = line.Aparelho != null ? line.Aparelho.Cor : null,
line.PlanoContrato, line.PlanoContrato,
line.Status, line.Status,
line.Skil, line.Skil,
@ -588,6 +605,10 @@ namespace line_gestao_api.Controllers
Chip = x.Chip, Chip = x.Chip,
Cliente = x.Cliente, Cliente = x.Cliente,
Usuario = x.Usuario, Usuario = x.Usuario,
CentroDeCustos = x.CentroDeCustos,
SetorNome = x.SetorNome,
AparelhoNome = x.AparelhoNome,
AparelhoCor = x.AparelhoCor,
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
Status = x.Status, Status = x.Status,
Skil = x.Skil, Skil = x.Skil,
@ -657,6 +678,10 @@ namespace line_gestao_api.Controllers
Chip = x.Chip, Chip = x.Chip,
Cliente = x.Cliente, Cliente = x.Cliente,
Usuario = x.Usuario, Usuario = x.Usuario,
CentroDeCustos = x.CentroDeCustos,
SetorNome = x.Setor != null ? x.Setor.Nome : null,
AparelhoNome = x.Aparelho != null ? x.Aparelho.Nome : null,
AparelhoCor = x.Aparelho != null ? x.Aparelho.Cor : null,
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
Status = x.Status, Status = x.Status,
Skil = x.Skil, Skil = x.Skil,
@ -688,7 +713,11 @@ namespace line_gestao_api.Controllers
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id) public async Task<ActionResult<MobileLineDetailDto>> GetById(Guid id)
{ {
var x = await _db.MobileLines.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id); var x = await _db.MobileLines
.AsNoTracking()
.Include(a => a.Setor)
.Include(a => a.Aparelho)
.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound(); if (x == null) return NotFound();
var vigencia = await FindVigenciaByMobileLineAsync(x, null, asNoTracking: true); var vigencia = await FindVigenciaByMobileLineAsync(x, null, asNoTracking: true);
return Ok(ToDetailDto(x, vigencia)); return Ok(ToDetailDto(x, vigencia));
@ -790,6 +819,8 @@ namespace line_gestao_api.Controllers
lineToPersist.Cliente = ensuredTenant.NomeOficial; lineToPersist.Cliente = ensuredTenant.NomeOficial;
} }
await ApplySetorAndAparelhoToLineAsync(lineToPersist, req);
var vigencia = await UpsertVigenciaFromMobileLineAsync( var vigencia = await UpsertVigenciaFromMobileLineAsync(
lineToPersist, lineToPersist,
req.DtEfetivacaoServico, req.DtEfetivacaoServico,
@ -923,6 +954,7 @@ namespace line_gestao_api.Controllers
Linha = linhaLimpa, Linha = linhaLimpa,
Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo, Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo,
Usuario = entry.Usuario?.Trim(), Usuario = entry.Usuario?.Trim(),
CentroDeCustos = NormalizeOptionalText(entry.CentroDeCustos),
Status = entry.Status?.Trim(), Status = entry.Status?.Trim(),
Skil = entry.Skil?.Trim(), Skil = entry.Skil?.Trim(),
Modalidade = entry.Modalidade?.Trim(), Modalidade = entry.Modalidade?.Trim(),
@ -973,6 +1005,8 @@ namespace line_gestao_api.Controllers
newLine.Cliente = ensuredTenant.NomeOficial; newLine.Cliente = ensuredTenant.NomeOficial;
} }
await ApplySetorAndAparelhoToLineAsync(newLine, entry);
_db.MobileLines.Add(newLine); _db.MobileLines.Add(newLine);
var vigencia = await UpsertVigenciaFromMobileLineAsync( var vigencia = await UpsertVigenciaFromMobileLineAsync(
@ -1293,11 +1327,49 @@ namespace line_gestao_api.Controllers
// ✅ 6. UPDATE // ✅ 6. UPDATE
// ========================================================== // ==========================================================
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Roles = "sysadmin,gestor")] [Authorize(Roles = "sysadmin,gestor,cliente")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req) public async Task<IActionResult> Update(Guid id, [FromBody] UpdateMobileLineRequest req)
{ {
var x = await _db.MobileLines.FirstOrDefaultAsync(a => a.Id == id); var x = await _db.MobileLines
.Include(a => a.Setor)
.Include(a => a.Aparelho)
.FirstOrDefaultAsync(a => a.Id == id);
if (x == null) return NotFound(); if (x == null) return NotFound();
var canManageFullLine = User.IsInRole(AppRoles.SysAdmin) || User.IsInRole(AppRoles.Gestor);
if (!canManageFullLine)
{
x.Usuario = NormalizeOptionalText(req.Usuario);
x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
await ApplyAparelhoToLineAsync(
x,
x.TenantId,
req.AparelhoId,
req.AparelhoNome,
req.AparelhoCor,
req.AparelhoImei);
await UpsertVigenciaFromMobileLineAsync(
x,
dtEfetivacaoServico: null,
dtTerminoFidelizacao: null,
overrideDates: false,
previousLinha: null);
x.UpdatedAt = DateTime.UtcNow;
try
{
await _db.SaveChangesAsync();
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
}
catch (DbUpdateException)
{
return Conflict(new { message = "Conflito ao salvar." });
}
return NoContent();
}
var previousLinha = x.Linha; var previousLinha = x.Linha;
var previousCliente = x.Cliente; var previousCliente = x.Cliente;
@ -1314,9 +1386,10 @@ namespace line_gestao_api.Controllers
var newChip = OnlyDigits(req.Chip); var newChip = OnlyDigits(req.Chip);
x.Chip = string.IsNullOrWhiteSpace(newChip) ? null : newChip; x.Chip = string.IsNullOrWhiteSpace(newChip) ? null : newChip;
x.Cliente = req.Cliente?.Trim(); x.Cliente = NormalizeOptionalText(req.Cliente);
x.Usuario = req.Usuario?.Trim(); x.Usuario = NormalizeOptionalText(req.Usuario);
x.PlanoContrato = req.PlanoContrato?.Trim(); x.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
x.PlanoContrato = NormalizeOptionalText(req.PlanoContrato);
x.FranquiaVivo = req.FranquiaVivo; x.FranquiaVivo = req.FranquiaVivo;
x.ValorPlanoVivo = req.ValorPlanoVivo; x.ValorPlanoVivo = req.ValorPlanoVivo;
x.GestaoVozDados = req.GestaoVozDados; x.GestaoVozDados = req.GestaoVozDados;
@ -1332,16 +1405,16 @@ namespace line_gestao_api.Controllers
x.ValorContratoLine = req.ValorContratoLine; x.ValorContratoLine = req.ValorContratoLine;
x.Desconto = req.Desconto; x.Desconto = req.Desconto;
x.Lucro = req.Lucro; x.Lucro = req.Lucro;
x.Status = req.Status?.Trim(); x.Status = NormalizeOptionalText(req.Status);
x.DataBloqueio = ToUtc(req.DataBloqueio); x.DataBloqueio = ToUtc(req.DataBloqueio);
x.Skil = req.Skil?.Trim(); x.Skil = NormalizeOptionalText(req.Skil);
x.Modalidade = req.Modalidade?.Trim(); x.Modalidade = NormalizeOptionalText(req.Modalidade);
x.Cedente = req.Cedente?.Trim(); x.Cedente = NormalizeOptionalText(req.Cedente);
x.Solicitante = req.Solicitante?.Trim(); x.Solicitante = NormalizeOptionalText(req.Solicitante);
x.DataEntregaOpera = ToUtc(req.DataEntregaOpera); x.DataEntregaOpera = ToUtc(req.DataEntregaOpera);
x.DataEntregaCliente = ToUtc(req.DataEntregaCliente); x.DataEntregaCliente = ToUtc(req.DataEntregaCliente);
x.VencConta = req.VencConta?.Trim(); x.VencConta = NormalizeOptionalText(req.VencConta);
x.TipoDeChip = req.TipoDeChip?.Trim(); x.TipoDeChip = NormalizeOptionalText(req.TipoDeChip);
var previousClienteNormalized = string.IsNullOrWhiteSpace(previousCliente) ? null : previousCliente.Trim(); var previousClienteNormalized = string.IsNullOrWhiteSpace(previousCliente) ? null : previousCliente.Trim();
var clienteAtualIsReserva = IsReservaValue(x.Cliente); var clienteAtualIsReserva = IsReservaValue(x.Cliente);
@ -1371,6 +1444,15 @@ namespace line_gestao_api.Controllers
x.Cliente = ensuredTenant.NomeOficial; x.Cliente = ensuredTenant.NomeOficial;
} }
await ApplySetorToLineAsync(x, x.TenantId, req.SetorId, req.SetorNome);
await ApplyAparelhoToLineAsync(
x,
x.TenantId,
req.AparelhoId,
req.AparelhoNome,
req.AparelhoCor,
req.AparelhoImei);
await UpsertVigenciaFromMobileLineAsync( await UpsertVigenciaFromMobileLineAsync(
x, x,
req.DtEfetivacaoServico, req.DtEfetivacaoServico,
@ -1389,6 +1471,128 @@ namespace line_gestao_api.Controllers
return NoContent(); return NoContent();
} }
[HttpPost("{id:guid}/aparelho/anexos")]
[Authorize(Roles = "sysadmin,gestor,cliente")]
[Consumes("multipart/form-data")]
[RequestSizeLimit(25_000_000)]
public async Task<ActionResult<MobileLineDetailDto>> UploadAparelhoAnexos(
Guid id,
[FromForm] UploadAparelhoAnexosForm form)
{
var hasNotaFiscal = form.NotaFiscal != null && form.NotaFiscal.Length > 0;
var hasRecibo = form.Recibo != null && form.Recibo.Length > 0;
if (!hasNotaFiscal && !hasRecibo)
{
return BadRequest(new { message = "Envie ao menos um arquivo (Nota Fiscal ou Recibo)." });
}
var line = await _db.MobileLines
.Include(x => x.Aparelho)
.FirstOrDefaultAsync(x => x.Id == id);
if (line == null)
{
return NotFound();
}
var tenantId = line.TenantId != Guid.Empty
? line.TenantId
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
if (tenantId == Guid.Empty)
{
return BadRequest(new { message = "Tenant inválido para salvar anexo." });
}
if (line.TenantId == Guid.Empty)
{
line.TenantId = tenantId;
}
var aparelho = await EnsureLineHasAparelhoAsync(line, tenantId);
try
{
if (hasNotaFiscal && form.NotaFiscal != null)
{
aparelho.NotaFiscalArquivoPath = await SaveAparelhoAttachmentAsync(
form.NotaFiscal,
tenantId,
line.Id,
"nota-fiscal",
aparelho.NotaFiscalArquivoPath);
}
if (hasRecibo && form.Recibo != null)
{
aparelho.ReciboArquivoPath = await SaveAparelhoAttachmentAsync(
form.Recibo,
tenantId,
line.Id,
"recibo",
aparelho.ReciboArquivoPath);
}
}
catch (InvalidOperationException ex)
{
return BadRequest(new { message = ex.Message });
}
catch (IOException)
{
return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Falha ao salvar anexo no servidor." });
}
line.UpdatedAt = DateTime.UtcNow;
aparelho.UpdatedAt = DateTime.UtcNow;
await _db.SaveChangesAsync();
await _vigenciaNotificationSyncService.SyncCurrentTenantAsync();
var vigencia = await FindVigenciaByMobileLineAsync(line, null, asNoTracking: true);
return Ok(ToDetailDto(line, vigencia));
}
[HttpGet("{id:guid}/aparelho/anexos/{tipo}")]
[Authorize(Roles = "sysadmin,gestor,cliente")]
public async Task<IActionResult> DownloadAparelhoAnexo(Guid id, string tipo)
{
var line = await _db.MobileLines
.AsNoTracking()
.Include(x => x.Aparelho)
.FirstOrDefaultAsync(x => x.Id == id);
if (line == null || line.Aparelho == null)
{
return NotFound();
}
var normalizedTipo = (tipo ?? string.Empty).Trim().ToLowerInvariant();
var relativePath = normalizedTipo switch
{
"nota-fiscal" or "nota_fiscal" or "notafiscal" => line.Aparelho.NotaFiscalArquivoPath,
"recibo" => line.Aparelho.ReciboArquivoPath,
_ => null
};
if (string.IsNullOrWhiteSpace(relativePath))
{
return NotFound();
}
var fullPath = TryResolveAttachmentFullPath(relativePath);
if (string.IsNullOrWhiteSpace(fullPath) || !System.IO.File.Exists(fullPath))
{
return NotFound();
}
if (!FileContentTypeProvider.TryGetContentType(fullPath, out var contentType))
{
contentType = "application/octet-stream";
}
var downloadName = Path.GetFileName(fullPath);
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return File(stream, contentType, downloadName);
}
// ========================================================== // ==========================================================
// ✅ 7. DELETE // ✅ 7. DELETE
// ========================================================== // ==========================================================
@ -4098,6 +4302,7 @@ namespace line_gestao_api.Controllers
line.Linha = linhaLimpa; line.Linha = linhaLimpa;
line.Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo; line.Chip = string.IsNullOrWhiteSpace(chipLimpo) ? null : chipLimpo;
line.Usuario = req.Usuario?.Trim(); line.Usuario = req.Usuario?.Trim();
line.CentroDeCustos = NormalizeOptionalText(req.CentroDeCustos);
line.Status = req.Status?.Trim(); line.Status = req.Status?.Trim();
line.Skil = req.Skil?.Trim(); line.Skil = req.Skil?.Trim();
line.Modalidade = req.Modalidade?.Trim(); line.Modalidade = req.Modalidade?.Trim();
@ -4131,6 +4336,330 @@ namespace line_gestao_api.Controllers
line.UpdatedAt = now; line.UpdatedAt = now;
} }
private async Task ApplySetorAndAparelhoToLineAsync(MobileLine line, CreateMobileLineDto req)
{
var tenantId = line.TenantId != Guid.Empty
? line.TenantId
: (_tenantProvider.ActorTenantId ?? Guid.Empty);
if (tenantId == Guid.Empty)
{
return;
}
if (line.TenantId == Guid.Empty)
{
line.TenantId = tenantId;
}
await ApplySetorToLineAsync(line, tenantId, req.SetorId, req.SetorNome);
await ApplyAparelhoToLineAsync(
line,
tenantId,
req.AparelhoId,
req.AparelhoNome,
req.AparelhoCor,
req.AparelhoImei);
}
private async Task ApplySetorToLineAsync(MobileLine line, Guid tenantId, Guid? setorId, string? setorNome)
{
var hasSetorId = setorId.HasValue && setorId.Value != Guid.Empty;
var setorNomeInformado = setorNome != null;
var setorNomeNormalizado = NormalizeOptionalText(setorNome);
if (!hasSetorId && !setorNomeInformado)
{
return;
}
if (!hasSetorId && setorNomeInformado && string.IsNullOrWhiteSpace(setorNomeNormalizado))
{
line.SetorId = null;
line.Setor = null;
return;
}
var setor = await ResolveSetorAsync(tenantId, setorId, setorNomeNormalizado);
if (setor == null)
{
if (setorNomeInformado)
{
line.SetorId = null;
line.Setor = null;
}
return;
}
line.SetorId = setor.Id;
line.Setor = setor;
}
private async Task<Setor?> ResolveSetorAsync(Guid tenantId, Guid? setorId, string? setorNome)
{
if (setorId.HasValue && setorId.Value != Guid.Empty)
{
var byId = await _db.Setores.FirstOrDefaultAsync(s => s.TenantId == tenantId && s.Id == setorId.Value);
if (byId != null)
{
if (!string.IsNullOrWhiteSpace(setorNome) &&
!string.Equals(byId.Nome, setorNome, StringComparison.Ordinal))
{
byId.Nome = setorNome;
byId.UpdatedAt = DateTime.UtcNow;
}
return byId;
}
}
if (string.IsNullOrWhiteSpace(setorNome))
{
return null;
}
var normalized = NormalizeTenantKeyValue(setorNome);
var candidates = await _db.Setores.Where(s => s.TenantId == tenantId).ToListAsync();
var existing = candidates.FirstOrDefault(s =>
string.Equals(NormalizeTenantKeyValue(s.Nome), normalized, StringComparison.Ordinal));
if (existing != null)
{
if (!string.Equals(existing.Nome, setorNome, StringComparison.Ordinal))
{
existing.Nome = setorNome;
existing.UpdatedAt = DateTime.UtcNow;
}
return existing;
}
var now = DateTime.UtcNow;
var created = new Setor
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Nome = setorNome,
CreatedAt = now,
UpdatedAt = now
};
_db.Setores.Add(created);
return created;
}
private async Task ApplyAparelhoToLineAsync(
MobileLine line,
Guid tenantId,
Guid? aparelhoId,
string? aparelhoNome,
string? aparelhoCor,
string? aparelhoImei)
{
var hasAparelhoId = aparelhoId.HasValue && aparelhoId.Value != Guid.Empty;
var hasAnyPayload =
aparelhoNome != null ||
aparelhoCor != null ||
aparelhoImei != null;
if (!hasAparelhoId && !hasAnyPayload)
{
return;
}
var normalizedNome = NormalizeOptionalText(aparelhoNome);
var normalizedCor = NormalizeOptionalText(aparelhoCor);
var normalizedImei = NormalizeOptionalText(aparelhoImei);
var shouldDetach =
!hasAparelhoId &&
hasAnyPayload &&
string.IsNullOrWhiteSpace(normalizedNome) &&
string.IsNullOrWhiteSpace(normalizedCor) &&
string.IsNullOrWhiteSpace(normalizedImei);
if (shouldDetach)
{
line.AparelhoId = null;
line.Aparelho = null;
return;
}
var aparelho = await ResolveAparelhoAsync(tenantId, aparelhoId, line.AparelhoId);
if (aparelho == null)
{
var now = DateTime.UtcNow;
aparelho = new Aparelho
{
Id = Guid.NewGuid(),
TenantId = tenantId,
CreatedAt = now,
UpdatedAt = now
};
_db.Aparelhos.Add(aparelho);
}
if (aparelhoNome != null) aparelho.Nome = normalizedNome;
if (aparelhoCor != null) aparelho.Cor = normalizedCor;
if (aparelhoImei != null) aparelho.Imei = normalizedImei;
aparelho.UpdatedAt = DateTime.UtcNow;
line.AparelhoId = aparelho.Id;
line.Aparelho = aparelho;
}
private async Task<Aparelho?> ResolveAparelhoAsync(Guid tenantId, Guid? aparelhoId, Guid? lineAparelhoId)
{
if (aparelhoId.HasValue && aparelhoId.Value != Guid.Empty)
{
var byRequestId = await _db.Aparelhos
.FirstOrDefaultAsync(a => a.TenantId == tenantId && a.Id == aparelhoId.Value);
if (byRequestId != null)
{
return byRequestId;
}
}
if (lineAparelhoId.HasValue && lineAparelhoId.Value != Guid.Empty)
{
return await _db.Aparelhos
.FirstOrDefaultAsync(a => a.TenantId == tenantId && a.Id == lineAparelhoId.Value);
}
return null;
}
private async Task<Aparelho> EnsureLineHasAparelhoAsync(MobileLine line, Guid tenantId)
{
if (line.Aparelho != null && line.Aparelho.TenantId == tenantId)
{
if (line.AparelhoId != line.Aparelho.Id)
{
line.AparelhoId = line.Aparelho.Id;
}
return line.Aparelho;
}
var aparelho = await ResolveAparelhoAsync(tenantId, line.AparelhoId, line.AparelhoId);
if (aparelho == null)
{
var now = DateTime.UtcNow;
aparelho = new Aparelho
{
Id = Guid.NewGuid(),
TenantId = tenantId,
CreatedAt = now,
UpdatedAt = now
};
_db.Aparelhos.Add(aparelho);
}
line.AparelhoId = aparelho.Id;
line.Aparelho = aparelho;
return aparelho;
}
private async Task<string> SaveAparelhoAttachmentAsync(
IFormFile file,
Guid tenantId,
Guid lineId,
string attachmentKind,
string? previousRelativePath)
{
const long maxBytes = 15 * 1024 * 1024;
if (file.Length <= 0)
{
throw new InvalidOperationException("Arquivo de anexo inválido.");
}
if (file.Length > maxBytes)
{
throw new InvalidOperationException("O anexo excede o limite de 15MB.");
}
var extension = (Path.GetExtension(file.FileName) ?? string.Empty).Trim().ToLowerInvariant();
var allowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".pdf", ".png", ".jpg", ".jpeg", ".webp"
};
if (string.IsNullOrWhiteSpace(extension) || !allowedExtensions.Contains(extension))
{
throw new InvalidOperationException("Formato de arquivo inválido. Use PDF, PNG, JPG, JPEG ou WEBP.");
}
var targetDirectory = Path.Combine(
_aparelhoAttachmentsRootPath,
tenantId.ToString("N"),
lineId.ToString("N"),
attachmentKind);
Directory.CreateDirectory(targetDirectory);
var safeName = $"{DateTime.UtcNow:yyyyMMddHHmmssfff}_{Guid.NewGuid():N}{extension}";
var fullPath = Path.Combine(targetDirectory, safeName);
await using (var stream = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
await file.CopyToAsync(stream);
}
if (!string.IsNullOrWhiteSpace(previousRelativePath))
{
var oldPath = TryResolveAttachmentFullPath(previousRelativePath);
if (!string.IsNullOrWhiteSpace(oldPath) &&
!string.Equals(oldPath, fullPath, StringComparison.OrdinalIgnoreCase) &&
System.IO.File.Exists(oldPath))
{
try
{
System.IO.File.Delete(oldPath);
}
catch
{
// Falha ao remover arquivo antigo não deve impedir o fluxo principal.
}
}
}
var relativePath = Path.GetRelativePath(_aparelhoAttachmentsRootPath, fullPath);
return relativePath.Replace('\\', '/');
}
private string? TryResolveAttachmentFullPath(string? relativePath)
{
if (string.IsNullOrWhiteSpace(relativePath))
{
return null;
}
var normalized = relativePath
.Replace('\\', Path.DirectorySeparatorChar)
.TrimStart(Path.DirectorySeparatorChar, '/');
if (normalized.Contains("..", StringComparison.Ordinal))
{
return null;
}
var root = Path.GetFullPath(_aparelhoAttachmentsRootPath);
var fullPath = Path.GetFullPath(Path.Combine(root, normalized));
if (!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase))
{
return null;
}
return fullPath;
}
private static string? NormalizeOptionalText(string? value)
{
if (value == null)
{
return null;
}
var trimmed = value.Trim();
return trimmed.Length == 0 ? null : trimmed;
}
private async Task<Tenant?> EnsureTenantForClientAsync(string? rawClientName) private async Task<Tenant?> EnsureTenantForClientAsync(string? rawClientName)
{ {
var clientName = (rawClientName ?? string.Empty).Trim(); var clientName = (rawClientName ?? string.Empty).Trim();
@ -4408,6 +4937,15 @@ namespace line_gestao_api.Controllers
Chip = x.Chip, Chip = x.Chip,
Cliente = x.Cliente, Cliente = x.Cliente,
Usuario = x.Usuario, Usuario = x.Usuario,
CentroDeCustos = x.CentroDeCustos,
SetorId = x.SetorId,
SetorNome = x.Setor?.Nome,
AparelhoId = x.AparelhoId,
AparelhoNome = x.Aparelho?.Nome,
AparelhoCor = x.Aparelho?.Cor,
AparelhoImei = x.Aparelho?.Imei,
AparelhoNotaFiscalTemArquivo = !string.IsNullOrWhiteSpace(x.Aparelho?.NotaFiscalArquivoPath),
AparelhoReciboTemArquivo = !string.IsNullOrWhiteSpace(x.Aparelho?.ReciboArquivoPath),
PlanoContrato = x.PlanoContrato, PlanoContrato = x.PlanoContrato,
FranquiaVivo = x.FranquiaVivo, FranquiaVivo = x.FranquiaVivo,
ValorPlanoVivo = x.ValorPlanoVivo, ValorPlanoVivo = x.ValorPlanoVivo,

View File

@ -24,6 +24,8 @@ public class AppDbContext : IdentityDbContext<ApplicationUser, IdentityRole<Guid
// ✅ tabela para espelhar a planilha (GERAL) // ✅ tabela para espelhar a planilha (GERAL)
public DbSet<MobileLine> MobileLines => Set<MobileLine>(); public DbSet<MobileLine> MobileLines => Set<MobileLine>();
public DbSet<Setor> Setores => Set<Setor>();
public DbSet<Aparelho> Aparelhos => Set<Aparelho>();
// ✅ tabela para espelhar a aba MUREG // ✅ tabela para espelhar a aba MUREG
public DbSet<MuregLine> MuregLines => Set<MuregLine>(); 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 }); 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) // ✅ 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) // 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.HasIndex(x => new { x.TenantId, x.Linha }).IsUnique();
e.Property(x => x.CentroDeCustos).HasMaxLength(180);
// performance // performance
e.HasIndex(x => x.Chip); e.HasIndex(x => x.Chip);
e.HasIndex(x => x.Cliente); e.HasIndex(x => x.Cliente);
e.HasIndex(x => x.Usuario); e.HasIndex(x => x.Usuario);
e.HasIndex(x => x.CentroDeCustos);
e.HasIndex(x => x.Skil); e.HasIndex(x => x.Skil);
e.HasIndex(x => x.Status); 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<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<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<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<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)); modelBuilder.Entity<VigenciaLine>().HasQueryFilter(x => _tenantProvider.HasGlobalViewAccess || (_tenantProvider.ActorTenantId != null && x.TenantId == _tenantProvider.ActorTenantId));

View File

@ -12,6 +12,13 @@ namespace line_gestao_api.Dtos
public string? Chip { get; set; } // ICCID public string? Chip { get; set; } // ICCID
public string? Cliente { get; set; } // Obrigatório na validação do Controller public string? Cliente { get; set; } // Obrigatório na validação do Controller
public string? Usuario { 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 Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva public Guid? ReservaLineId { get; set; } // Reaproveita linha já existente na Reserva
// ========================== // ==========================

View File

@ -9,6 +9,10 @@
public string? Chip { get; set; } public string? Chip { get; set; }
public string? Cliente { get; set; } public string? Cliente { get; set; }
public string? Usuario { 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? PlanoContrato { get; set; }
public string? Status { get; set; } public string? Status { get; set; }
public string? Skil { get; set; } public string? Skil { get; set; }
@ -35,6 +39,15 @@
public string? Chip { get; set; } public string? Chip { get; set; }
public string? Cliente { get; set; } public string? Cliente { get; set; }
public string? Usuario { 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 string? PlanoContrato { get; set; }
public decimal? FranquiaVivo { get; set; } public decimal? FranquiaVivo { get; set; }
@ -78,6 +91,13 @@
public string? Chip { get; set; } public string? Chip { get; set; }
public string? Cliente { get; set; } public string? Cliente { get; set; }
public string? Usuario { 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 string? PlanoContrato { get; set; }
public decimal? FranquiaVivo { 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"); 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 => modelBuilder.Entity("line_gestao_api.Models.BillingClient", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -624,10 +670,17 @@ namespace line_gestao_api.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("uuid"); .HasColumnType("uuid");
b.Property<Guid?>("AparelhoId")
.HasColumnType("uuid");
b.Property<string>("Cedente") b.Property<string>("Cedente")
.HasMaxLength(150) .HasMaxLength(150)
.HasColumnType("character varying(150)"); .HasColumnType("character varying(150)");
b.Property<string>("CentroDeCustos")
.HasMaxLength(180)
.HasColumnType("character varying(180)");
b.Property<string>("Chip") b.Property<string>("Chip")
.HasMaxLength(40) .HasMaxLength(40)
.HasColumnType("character varying(40)"); .HasColumnType("character varying(40)");
@ -691,6 +744,9 @@ namespace line_gestao_api.Migrations
b.Property<decimal?>("Skeelo") b.Property<decimal?>("Skeelo")
.HasColumnType("numeric"); .HasColumnType("numeric");
b.Property<Guid?>("SetorId")
.HasColumnType("uuid");
b.Property<string>("Skil") b.Property<string>("Skil")
.HasMaxLength(80) .HasMaxLength(80)
.HasColumnType("character varying(80)"); .HasColumnType("character varying(80)");
@ -744,10 +800,16 @@ namespace line_gestao_api.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("AparelhoId");
b.HasIndex("Chip"); b.HasIndex("Chip");
b.HasIndex("CentroDeCustos");
b.HasIndex("Cliente"); b.HasIndex("Cliente");
b.HasIndex("SetorId");
b.HasIndex("Skil"); b.HasIndex("Skil");
b.HasIndex("Status"); b.HasIndex("Status");
@ -1370,6 +1432,36 @@ namespace line_gestao_api.Migrations
b.ToTable("ResumoVivoLineTotals"); 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 => modelBuilder.Entity("line_gestao_api.Models.Tenant", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1665,6 +1757,23 @@ namespace line_gestao_api.Migrations
.IsRequired(); .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 => modelBuilder.Entity("line_gestao_api.Models.MuregLine", b =>
{ {
b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine") b.HasOne("line_gestao_api.Models.MobileLine", "MobileLine")
@ -1715,6 +1824,11 @@ namespace line_gestao_api.Migrations
b.Navigation("ParcelamentoLine"); b.Navigation("ParcelamentoLine");
}); });
modelBuilder.Entity("line_gestao_api.Models.Aparelho", b =>
{
b.Navigation("MobileLines");
});
modelBuilder.Entity("line_gestao_api.Models.MobileLine", b => modelBuilder.Entity("line_gestao_api.Models.MobileLine", b =>
{ {
b.Navigation("Muregs"); b.Navigation("Muregs");
@ -1729,6 +1843,11 @@ namespace line_gestao_api.Migrations
{ {
b.Navigation("MonthValues"); b.Navigation("MonthValues");
}); });
modelBuilder.Entity("line_gestao_api.Models.Setor", b =>
{
b.Navigation("MobileLines");
});
#pragma warning restore 612, 618 #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; } public string? Cliente { get; set; }
[MaxLength(200)] [MaxLength(200)]
public string? Usuario { get; set; } 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)] [MaxLength(200)]
public string? PlanoContrato { get; set; } 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>();
}