From 852eab0c32f1fc297b2459f506bacc95c89a6ad1 Mon Sep 17 00:00:00 2001 From: Leandro Hernan Rojas Date: Mon, 18 Aug 2025 09:28:05 -0300 Subject: [PATCH] Add StockScanController --- .../Stock/LSStockScanController.cs | 34 ++++ .../obj/Debug/net8.0/ApiEndpoints.json | 26 +++ .../Services/Stock/StockScanService.cs | 186 ++++++++++-------- 3 files changed, 165 insertions(+), 81 deletions(-) diff --git a/phronCare.API/Controllers/Stock/LSStockScanController.cs b/phronCare.API/Controllers/Stock/LSStockScanController.cs index 14f6721..6d50698 100644 --- a/phronCare.API/Controllers/Stock/LSStockScanController.cs +++ b/phronCare.API/Controllers/Stock/LSStockScanController.cs @@ -2,6 +2,7 @@ using Domain.Dtos.Stock; // StockItemSearchParams, StockItemScanResultDto using Domain.Generics; // PagedResult using Microsoft.AspNetCore.Mvc; +using Transversal.Services; namespace API.Controllers.Stock { @@ -43,5 +44,38 @@ namespace API.Controllers.Stock var result = await _service.SearchParsedAsync(searchParams); return Ok(result); } + + // DTO liviano para el request RAW + public record StockScanRawRequest(string Raw, int LocationId, int Page = 1, int PageSize = 10); + + /// + /// Recibe un escaneo RAW (GS1-128/DataMatrix), lo parsea y ejecuta la búsqueda paginada. + /// + [HttpPost("parse-and-search")] + public async Task>> ParseAndSearch([FromBody] StockScanRawRequest req) + { + if (string.IsNullOrWhiteSpace(req.Raw)) + return BadRequest("Raw is required."); + + // 1) Parseo GS1 en backend + var parsed = Gs1CodeParser.Parse(req.Raw.Trim()); + + // 2) Armar parámetros "ya parseados" + var sp = new StockItemParsedSearchParams + { + Gtin = string.IsNullOrWhiteSpace(parsed.Gtin) ? parsed.Variant : parsed.Gtin, // (22) como fallback + Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, + Expiration = parsed.ExpirationDate.HasValue ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) : null, + Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, + LocationId = req.LocationId, + Page = req.Page, + PageSize = req.PageSize + }; + + // 3) Delegar a la misma lógica que ya tenés implementada + var result = await _service.SearchParsedAsync(sp); + return Ok(result); + } + } } diff --git a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json index 228b6ae..cbed662 100644 --- a/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json +++ b/phronCare.API/obj/Debug/net8.0/ApiEndpoints.json @@ -1226,6 +1226,32 @@ ], "ReturnTypes": [] }, + { + "ContainingType": "API.Controllers.Stock.LSStockScanController", + "Method": "ParseAndSearch", + "RelativePath": "api/LSStockScan/parse-and-search", + "HttpMethod": "POST", + "IsController": true, + "Order": 0, + "Parameters": [ + { + "Name": "req", + "Type": "API.Controllers.Stock.LSStockScanController\u002BStockScanRawRequest", + "IsRequired": true + } + ], + "ReturnTypes": [ + { + "Type": "Domain.Generics.PagedResult\u00601[[Domain.Dtos.Stock.StockItemScanResultDto, Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]", + "MediaTypes": [ + "text/plain", + "application/json", + "text/json" + ], + "StatusCode": 200 + } + ] + }, { "ContainingType": "API.Controllers.Stock.LSStockScanController", "Method": "Search", diff --git a/phronCare.UIBlazor/Services/Stock/StockScanService.cs b/phronCare.UIBlazor/Services/Stock/StockScanService.cs index e63c45c..fdab276 100644 --- a/phronCare.UIBlazor/Services/Stock/StockScanService.cs +++ b/phronCare.UIBlazor/Services/Stock/StockScanService.cs @@ -11,90 +11,114 @@ public class StockScanService : IStockScanService { _http = http; } - public async Task ParseAndMatchAsync(string rawInput, int locationId) { - if (string.IsNullOrWhiteSpace(rawInput)) - return null; + if (string.IsNullOrWhiteSpace(rawInput)) return null; - try + var payload = new { Raw = rawInput, LocationId = locationId, Page = 1, PageSize = 10 }; + var resp = await _http.PostAsJsonAsync("/api/lsstockscan/parse-and-search", payload); + if (!resp.IsSuccessStatusCode) return null; + + var pr = await resp.Content.ReadFromJsonAsync>(); + var first = pr?.Items?.FirstOrDefault(); + if (first is null) return null; + + return new StockItemSelectionDto { - var parsed = new Gs1ScanResult(); - //var parsed = ""; - var raw = rawInput.Trim(); - - bool hasParsedAis = !string.IsNullOrWhiteSpace(parsed.Lot) - || parsed.ExpirationDate.HasValue - || !string.IsNullOrWhiteSpace(parsed.Serial) - || !string.IsNullOrWhiteSpace(parsed.Variant); // incluir (22) - - string? gtinToSend = parsed.Gtin ?? parsed.Variant; // (22) como fallback - if (gtinToSend is null && !hasParsedAis && IsPlainCode(raw)) - gtinToSend = raw; // código plano tipeado (factory/regulatory) - - - // 3. Armar parámetros de búsqueda - var sp = new StockItemParsedSearchParams - { - Gtin = gtinToSend, - Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, - Expiration = parsed.ExpirationDate.HasValue - ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) - : null, - Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, - LocationId = locationId, - Page = 1, - PageSize = 10 - }; - - // 4. Log para depuración (quitar en producción) - Console.WriteLine($"[ParseAndMatchAsync] Gtin={sp.Gtin}, Batch={sp.Batch}, Exp={sp.Expiration}, Serial={sp.Serial}, Loc={sp.LocationId}"); - - // 5. Llamar a la API - var resp = await _http.PostAsJsonAsync("/api/lsstockscan/search-parsed", sp); - if (!resp.IsSuccessStatusCode) - { - var err = await resp.Content.ReadAsStringAsync(); - Console.WriteLine($"[ParseAndMatchAsync] API devolvió error {resp.StatusCode}: {err}"); - return null; - } - - // 6. Leer resultado - var pr = await resp.Content.ReadFromJsonAsync>(); - var first = pr?.Items?.FirstOrDefault(); - if (first == null) - { - Console.WriteLine("[ParseAndMatchAsync] No se encontró ningún ítem que coincida."); - return null; - } - - // 7. Mapear a DTO de selección - return new StockItemSelectionDto - { - StockItemId = first.StockItemId, - ProductId = first.ProductId, - ProductName = first.ProductName, - Batch = first.Batch ?? string.Empty, - Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue), - Quantity = first.AvailableQty, - LocationId = first.LocationId ?? 0 - }; - } - catch (Exception ex) - { - Console.WriteLine($"[ParseAndMatchAsync] Error inesperado: {ex}"); - throw; - } - - } - private static bool IsPlainCode(string s) - { - // sin FNC1 ($), sin espacios y sin prefijos AI típicos - if (s.Contains('$') || s.Contains((char)29) || s.Contains(' ')) return false; - // evita raws que empiezan como AIs "01","10","17","21","22" - var prefix = s.Length >= 2 ? s[..2] : s; - if (prefix is "01" or "10" or "11" or "17" or "21" or "22") return false; - // permite letras/dígitos y algunos separadores comunes - return s.All(c => char.IsLetterOrDigit(c) || c is '-' or '/' or '_'); + StockItemId = first.StockItemId, + ProductId = first.ProductId, + ProductName = first.ProductName, + Batch = first.Batch ?? string.Empty, + Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue), + Quantity = first.AvailableQty, + LocationId = first.LocationId ?? locationId, + Serial = first.Serial // si lo devolvés en el DTO de scan + }; } + + //public async Task ParseAndMatchAsync(string rawInput, int locationId) + //{ + // if (string.IsNullOrWhiteSpace(rawInput)) + // return null; + + // try + // { + // var parsed = new Gs1ScanResult(); + // //var parsed = ""; + // var raw = rawInput.Trim(); + + // bool hasParsedAis = !string.IsNullOrWhiteSpace(parsed.Lot) + // || parsed.ExpirationDate.HasValue + // || !string.IsNullOrWhiteSpace(parsed.Serial) + // || !string.IsNullOrWhiteSpace(parsed.Variant); // incluir (22) + + // string? gtinToSend = parsed.Gtin ?? parsed.Variant; // (22) como fallback + // if (gtinToSend is null && !hasParsedAis && IsPlainCode(raw)) + // gtinToSend = raw; // código plano tipeado (factory/regulatory) + + + // // 3. Armar parámetros de búsqueda + // var sp = new StockItemParsedSearchParams + // { + // Gtin = gtinToSend, + // Batch = string.IsNullOrWhiteSpace(parsed.Lot) ? null : parsed.Lot, + // Expiration = parsed.ExpirationDate.HasValue + // ? DateOnly.FromDateTime(parsed.ExpirationDate.Value) + // : null, + // Serial = string.IsNullOrWhiteSpace(parsed.Serial) ? null : parsed.Serial, + // LocationId = locationId, + // Page = 1, + // PageSize = 10 + // }; + + // // 4. Log para depuración (quitar en producción) + // Console.WriteLine($"[ParseAndMatchAsync] Gtin={sp.Gtin}, Batch={sp.Batch}, Exp={sp.Expiration}, Serial={sp.Serial}, Loc={sp.LocationId}"); + + // // 5. Llamar a la API + // var resp = await _http.PostAsJsonAsync("/api/lsstockscan/search-parsed", sp); + // if (!resp.IsSuccessStatusCode) + // { + // var err = await resp.Content.ReadAsStringAsync(); + // Console.WriteLine($"[ParseAndMatchAsync] API devolvió error {resp.StatusCode}: {err}"); + // return null; + // } + + // // 6. Leer resultado + // var pr = await resp.Content.ReadFromJsonAsync>(); + // var first = pr?.Items?.FirstOrDefault(); + // if (first == null) + // { + // Console.WriteLine("[ParseAndMatchAsync] No se encontró ningún ítem que coincida."); + // return null; + // } + + // // 7. Mapear a DTO de selección + // return new StockItemSelectionDto + // { + // StockItemId = first.StockItemId, + // ProductId = first.ProductId, + // ProductName = first.ProductName, + // Batch = first.Batch ?? string.Empty, + // Expiration = first.Expiration?.ToDateTime(TimeOnly.MinValue), + // Quantity = first.AvailableQty, + // LocationId = first.LocationId ?? 0 + // }; + // } + // catch (Exception ex) + // { + // Console.WriteLine($"[ParseAndMatchAsync] Error inesperado: {ex}"); + // throw; + // } + + //} + //private static bool IsPlainCode(string s) + //{ + // // sin FNC1 ($), sin espacios y sin prefijos AI típicos + // if (s.Contains('$') || s.Contains((char)29) || s.Contains(' ')) return false; + // // evita raws que empiezan como AIs "01","10","17","21","22" + // var prefix = s.Length >= 2 ? s[..2] : s; + // if (prefix is "01" or "10" or "11" or "17" or "21" or "22") return false; + // // permite letras/dígitos y algunos separadores comunes + // return s.All(c => char.IsLetterOrDigit(c) || c is '-' or '/' or '_'); + //} }