2025-09-04 18:15:15 -03:00
|
|
|
|
using Domain.Constants;
|
|
|
|
|
|
using Domain.Dtos.Stock;
|
|
|
|
|
|
using Domain.Entities;
|
2025-09-05 16:31:58 -03:00
|
|
|
|
using Domain.Generics;
|
2026-03-11 23:35:51 -03:00
|
|
|
|
using Microsoft.Data.SqlClient;
|
2025-09-04 18:15:15 -03:00
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
|
using Models.Helpers;
|
|
|
|
|
|
using Models.Interfaces;
|
|
|
|
|
|
using Models.Models;
|
2026-03-11 23:35:51 -03:00
|
|
|
|
using System.Data;
|
2025-09-05 16:31:58 -03:00
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Text;
|
2025-09-04 18:15:15 -03:00
|
|
|
|
|
|
|
|
|
|
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})"
|
|
|
|
|
|
};
|
2025-09-05 16:31:58 -03:00
|
|
|
|
// ===========================
|
|
|
|
|
|
// 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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-03-11 23:35:51 -03:00
|
|
|
|
public async Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(
|
|
|
|
|
|
IEnumerable<int> stockItemIds,
|
|
|
|
|
|
int? ignoreExpeditionId)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Normalización defensiva
|
|
|
|
|
|
var ids = (stockItemIds ?? Enumerable.Empty<int>())
|
|
|
|
|
|
.Where(x => x > 0)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
if (ids.Count == 0)
|
|
|
|
|
|
return new List<StockItemExpeditionConflictDto>();
|
|
|
|
|
|
|
|
|
|
|
|
// TVP: dbo.PhLSM_StockItemIdList(stockitem_id int not null)
|
|
|
|
|
|
var tvp = new DataTable();
|
|
|
|
|
|
tvp.Columns.Add("stockitem_id", typeof(int));
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var id in ids)
|
|
|
|
|
|
tvp.Rows.Add(id);
|
|
|
|
|
|
|
|
|
|
|
|
var results = new List<StockItemExpeditionConflictDto>();
|
|
|
|
|
|
|
|
|
|
|
|
// Usamos la conexión del DbContext (no creamos otra)
|
|
|
|
|
|
var conn = _context.Database.GetDbConnection();
|
|
|
|
|
|
if (conn.State != ConnectionState.Open)
|
|
|
|
|
|
await _context.Database.OpenConnectionAsync();
|
|
|
|
|
|
|
|
|
|
|
|
await using var cmd = conn.CreateCommand();
|
|
|
|
|
|
cmd.CommandText = "dbo.PhLSM_Expedition_CheckStockItemConflicts";
|
|
|
|
|
|
cmd.CommandType = CommandType.StoredProcedure;
|
|
|
|
|
|
|
|
|
|
|
|
// Param TVP
|
|
|
|
|
|
var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
|
|
|
|
|
|
{
|
|
|
|
|
|
TypeName = "dbo.PhLSM_StockItemIdList",
|
|
|
|
|
|
Value = tvp
|
|
|
|
|
|
};
|
|
|
|
|
|
cmd.Parameters.Add(pIds);
|
|
|
|
|
|
|
|
|
|
|
|
// Param opcional para edición
|
|
|
|
|
|
var pIgnore = new SqlParameter("@IgnoreExpeditionId", SqlDbType.Int)
|
|
|
|
|
|
{
|
|
|
|
|
|
Value = ignoreExpeditionId.HasValue ? ignoreExpeditionId.Value : DBNull.Value
|
|
|
|
|
|
};
|
|
|
|
|
|
cmd.Parameters.Add(pIgnore);
|
|
|
|
|
|
|
|
|
|
|
|
await using var reader = await cmd.ExecuteReaderAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// Ordinals por nombre (más robusto ante cambios de orden)
|
|
|
|
|
|
var ordStockItemId = reader.GetOrdinal("StockitemId");
|
|
|
|
|
|
var ordExpId = reader.GetOrdinal("ExpeditionId");
|
|
|
|
|
|
var ordExpNum = reader.GetOrdinal("Expeditionnumber");
|
|
|
|
|
|
var ordStatus = reader.GetOrdinal("Status");
|
|
|
|
|
|
|
|
|
|
|
|
while (await reader.ReadAsync())
|
|
|
|
|
|
{
|
|
|
|
|
|
results.Add(new StockItemExpeditionConflictDto
|
|
|
|
|
|
{
|
|
|
|
|
|
StockitemId = reader.GetInt32(ordStockItemId),
|
|
|
|
|
|
ExpeditionId = reader.GetInt32(ordExpId),
|
|
|
|
|
|
Expeditionnumber = reader.IsDBNull(ordExpNum) ? string.Empty : reader.GetString(ordExpNum),
|
|
|
|
|
|
Status = reader.GetInt32(ordStatus)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return results;
|
|
|
|
|
|
}
|
2025-09-05 16:31:58 -03:00
|
|
|
|
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(" ", "");
|
|
|
|
|
|
}
|
2026-03-14 21:23:06 -03:00
|
|
|
|
public async Task MarkInTransitAsync(int expeditionId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var header = await _context.PhLsmExpeditionHeaders
|
|
|
|
|
|
.FirstOrDefaultAsync(x => x.Id == expeditionId);
|
|
|
|
|
|
|
|
|
|
|
|
if (header == null)
|
|
|
|
|
|
throw new KeyNotFoundException($"No se encontró la expedición con ID {expeditionId}.");
|
2025-09-05 16:31:58 -03:00
|
|
|
|
|
2026-03-14 21:23:06 -03:00
|
|
|
|
if (header.Status != (int)ExpeditionStatus.Emitida)
|
|
|
|
|
|
throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");
|
|
|
|
|
|
|
|
|
|
|
|
header.Status = (int)ExpeditionStatus.EnTransito;
|
|
|
|
|
|
header.Modifiedat = DateTime.Now;
|
|
|
|
|
|
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
}
|
2025-09-04 18:15:15 -03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|