phronCare/Models/Repositories/Stock/PhLSMExpeditionRepository.cs
Leandro Hernan Rojas be690849fd
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 6m27s
Update Expediciones UI
2025-09-05 16:31:58 -03:00

346 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Domain.Constants;
using Domain.Dtos.Stock;
using Domain.Entities;
using Domain.Generics;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
using System.Globalization;
using System.Text;
namespace Models.Repositories.Stock
{
public class PhLSMExpeditionRepository(
PhronCareOperationsHubContext context,
IPhSFormSeriesRepository formSeriesRepository) : IExpeditionRepository
{
private readonly PhronCareOperationsHubContext _context = context;
private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;
/// <summary>
/// Crea la expedición completa (header + details) con numeración de serie y estado emitido.
/// </summary>
public async Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(
ELSExpeditionHeader expedition, int formSeriesId)
{
using var tx = await _context.Database.BeginTransactionAsync();
try
{
// 1) Numeración (EX-00000000) mismo patrón que Quotes
var next = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
var series = await _formSeriesRepository.GetByIdAsync(formSeriesId)
?? throw new InvalidOperationException("Serie no encontrada");
var number = $"{series.Letter}-{next:D8}";
var issuedAt = DateTime.Now;
// 2) Completar datos de emisión en el agregado de dominio
expedition.Expeditionnumber = number;
expedition.Issuedate = issuedAt;
expedition.Status = (int)ExpeditionStatus.Emitida;
// 3) Mapear grafo completo Domain -> EF (Header + Details)
// Igual que haces en CreateFullQuoteAsync con EntityMapper.MapEntity(...)
var headerEntity = EntityMapper.MapEntity<ELSExpeditionHeader, PhLsmExpeditionHeader>(expedition);
// 4) Persistir de una (header + colecciones) y confirmar
_context.PhLsmExpeditionHeaders.Add(headerEntity);
await _context.SaveChangesAsync();
await tx.CommitAsync();
return (headerEntity.Id, headerEntity.Expeditionnumber);
}
catch
{
await tx.RollbackAsync();
throw;
}
}
/// <summary>
/// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
/// </summary>
public async Task<ExpeditionDto?> GetDtoByIdAsync(int id)
{
// 1) Header + detalles
var header = await _context.PhLsmExpeditionHeaders
.AsNoTracking()
.Include(h => h.PhLsmExpeditionDetails)
.FirstOrDefaultAsync(h => h.Id == id);
if (header is null)
return null;
// 2) Resolver productos (un solo round-trip)
var productIds = header.PhLsmExpeditionDetails
.Select(d => d.ProductId)
.Distinct()
.ToList();
var productMap = productIds.Count == 0
? new Dictionary<int, (string? Name, string? Descripcion, string? FactoryCode, string? ExternalCode, string? RegulatoryCode)>()
: await _context.PhLsmProducts
.Where(p => productIds.Contains(p.Id))
.Select(p => new
{
p.Id,
p.Name,
p.Descripcion,
p.FactoryCode, // código fábrica (preferido en impresión)
p.ExternalCode, // GTIN
p.RegulatoryCode // PM
})
.ToDictionaryAsync(
p => p.Id,
p => (p.Name, p.Descripcion, p.FactoryCode, p.ExternalCode, p.RegulatoryCode)
);
//// 3) Resolver nombres de ubicaciones (si corresponde)
//var locationIds = header.PhLsmExpeditionDetails
// .Select(d => d.LocationId)
// .Where(l => l.HasValue)
// .Select(l => l!.Value)
// .Distinct()
// .ToList();
//var locationMap = locationIds.Count == 0
// ? new Dictionary<int, (string Name, string? Address)>()
// : await _context.PhLsmStockLocations
// .Where(l => locationIds.Contains(l.Id))
// .Select(l => new { l.Id, l.Name, l.Address }) // Address opcional
// .ToDictionaryAsync(l => l.Id, l => (l.Name, l.Address));
// 4) Proyección a DTO (ítems)
var items = header.PhLsmExpeditionDetails.Select(d =>
{
productMap.TryGetValue(d.ProductId, out var p);
var productName = !string.IsNullOrWhiteSpace(p.Name) ? p.Name
: (!string.IsNullOrWhiteSpace(p.Descripcion) ? p.Descripcion : string.Empty);
//var locationName = (d.LocationId.HasValue && locationMap.TryGetValue(d.LocationId.Value, out var ln))
// ? ln.Name
// : null;
return new ExpeditionItemDto
{
Id = d.Id,
ProductId = d.ProductId,
FactoryCode = p.FactoryCode ?? string.Empty, // preferido para mostrar
ProductName = productName,
Quantity = d.Quantity,
Batch = d.Batch,
Serial = d.Serial,
Expiration = d.Expiration,
LocationId = d.LocationId,
LocationName = string.Empty, // locationName, // si lo querés mostrar
};
}).ToList();
// 5) Completar cabecera del DTO
var dto = new ExpeditionDto
{
Id = header.Id,
Expeditionnumber = header.Expeditionnumber,
Issuedate = header.Issuedate,
Status = header.Status,
StatusLabel = MapStatus(header.Status),
ExtrainfoJson = header.ExtrainfoJson, // se arma en el momento de imprimir, como definiste
Observations = header.Observations,
// Opcional: si el header tiene BusinessUnitId / SeriesId, podés resolver aquí sus códigos/nombres.
Items = items
};
//// 6) Si todos los detalles comparten la misma ubicación, reflejarla en cabecera (útil para impresión)
//var distinctLocs = items.Select(i => i.LocationId).Where(x => x.HasValue).Distinct().ToList();
//if (distinctLocs.Count == 1)
//{
// dto.LocationId = distinctLocs[0];
// if (dto.LocationId.HasValue && locationMap.TryGetValue(dto.LocationId.Value, out var ln))
// {
// dto.LocationName = ln.Name;
// // dto.LocationAddress = ln.Address; // si tu DTO lo contempla
// }
//}
return dto;
}
// ----- helpers -----
/// <summary>
/// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
/// </summary>
private static string MapStatus(int status) => status switch
{
1 => "Emitida",
2 => "En tránsito",
3 => "En destino",
4 => "Retorno",
5 => "Cerrada",
6 => "Anulada",
_ => $"Desconocido ({status})"
};
// ===========================
// BÚSQUEDA + PAGINACIÓN
// ===========================
public async Task<PagedResult<ExpeditionDto>> SearchAsync(
string? expeditionNumber,
string? status,
DateTime? issueDateFrom,
DateTime? issueDateTo,
int? locationId,
int page,
int pageSize)
{
if (page <= 0) page = 1;
if (pageSize <= 0) pageSize = 10;
// NOTE: ajustá el DbSet si tu entidad se llama distinto
var q = _context.PhLsmExpeditionHeaders
.AsNoTracking()
.AsQueryable();
// Número
if (!string.IsNullOrWhiteSpace(expeditionNumber))
{
var num = expeditionNumber.Trim();
q = q.Where(h => EF.Functions.Like(h.Expeditionnumber!, $"%{num}%"));
}
// Estado (acepta etiqueta o número)
if (!string.IsNullOrWhiteSpace(status))
{
var st = MapStatusLabelToInt(status);
if (st.HasValue) q = q.Where(h => h.Status == st.Value);
}
// Fechas (inclusivo)
if (issueDateFrom.HasValue)
{
var from = issueDateFrom.Value.Date;
q = q.Where(h => h.Issuedate >= from);
}
if (issueDateTo.HasValue)
{
var toExclusive = issueDateTo.Value.Date.AddDays(1);
q = q.Where(h => h.Issuedate < toExclusive);
}
// Ubicación (si la cabecera no la tiene, filtramos por detalles)
if (locationId.HasValue)
{
q = q.Where(h => h.PhLsmExpeditionDetails.Any(d => d.LocationId == locationId.Value));
}
var total = await q.CountAsync();
// Página (más recientes primero)
var headers = await q
.OrderByDescending(h => h.Issuedate)
.ThenByDescending(h => h.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(h => new ExpeditionDto
{
Id = h.Id,
Expeditionnumber = h.Expeditionnumber!,
Issuedate = h.Issuedate, // no-nullable
Status = h.Status,
StatusLabel = MapStatus(h.Status),
ExternalReference=h.ExternalReference,
ExtrainfoJson = h.ExtrainfoJson,
Observations = h.Observations,
Printcount = h.Printcount, // ← ajustá si tu entidad usa otro nombre
Createdat = h.Createdat, // ← idem
Modifiedat = h.Modifiedat, // ← idem
LocationId = 0, // por defecto (se resuelve si todos los ítems comparten)
LocationName = null
})
.ToListAsync();
if (headers.Count > 0)
{
var headerIds = headers.Select(x => x.Id).ToList();
// Distintas ubicaciones por header (en base a detalles)
var locsPerHeader = await _context.PhLsmExpeditionDetails
.AsNoTracking()
.Where(d => headerIds.Contains(d.ExpeditionId)) // ← ajustá si es HeaderId
.GroupBy(d => d.ExpeditionId)
.Select(g => new
{
HeaderId = g.Key,
DistinctLocs = g.Select(x => x.LocationId).Distinct().ToList()
})
.ToListAsync();
// Diccionario de nombres de ubicación
//var allLocIds = locsPerHeader.SelectMany(x => x.DistinctLocs).Distinct().ToList();
//var locNames = await _context.PhLsmStockLocations
// .AsNoTracking()
// .Where(l => allLocIds.Contains(l.Id))
// .Select(l => new { l.Id, l.Name })
// .ToDictionaryAsync(x => x.Id, x => x.Name);
//// Asignar LocationId/Name sólo si TODOS los items comparten la misma
//foreach (var h in headers)
//{
// var entry = locsPerHeader.FirstOrDefault(x => x.HeaderId == h.Id);
// if (entry is null) continue;
// if (entry.DistinctLocs.Count == 1)
// {
// var lid = entry.DistinctLocs[0];
// h.LocationId = lid;
// if (locNames.TryGetValue(lid, out var lname))
// h.LocationName = lname;
// }
// else
// {
// // varias ubicaciones: dejamos LocationId=0 y LocationName=null
// }
//}
}
return new PagedResult<ExpeditionDto>
{
Items = headers,
TotalItems = total,
Page = page,
PageSize = pageSize
};
}
private static int? MapStatusLabelToInt(string labelOrNumber)
{
if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;
if (int.TryParse(labelOrNumber, out var n) && n is >= 1 and <= 6)
return n;
var key = NormalizeKey(labelOrNumber);
return key switch
{
"emitida" => 1,
"entransito" => 2, // cubre "en transito" y "en tránsito"
"endestino" => 3,
"retorno" => 4,
"cerrada" => 5,
"anulada" => 6,
_ => (int?)null
};
}
private static string NormalizeKey(string s)
{
var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(norm.Length);
foreach (var ch in norm)
if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
sb.Append(ch);
return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", "");
}
}
}