using Domain.Constants; using Domain.Dtos.Stock; using Domain.Entities; using Microsoft.EntityFrameworkCore; using Models.Helpers; using Models.Interfaces; using Models.Models; namespace Models.Repositories.Stock { public class PhLSMExpeditionRepository( PhronCareOperationsHubContext context, IPhSFormSeriesRepository formSeriesRepository) : IExpeditionRepository { private readonly PhronCareOperationsHubContext _context = context; private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository; /// /// Crea la expedición completa (header + details) con numeración de serie y estado emitido. /// 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(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; } } /// /// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión. /// public async Task 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() : 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() // : 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 ----- /// /// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6). /// 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})" }; } }