From 8f81614922d0a51fccdbfb04ef6e4a3843e15d47 Mon Sep 17 00:00:00 2001 From: leandro Date: Fri, 27 Mar 2026 16:03:40 -0300 Subject: [PATCH] feat(deliverynote): add excel export with clinical snapshot parsing - Implement exportfiltered endpoint - Generate Excel using XLSXExportBase (EPPlus) - Map Delivery Note summary fields - Parse ExtraInfoJson into business columns (Professional, Institution, Patient, SurgeryDate) - Format dates for Excel - Keep export at header level (no items) Closes #46 --- Core/Interfaces/IDeliveryNoteDom.cs | 5 + Core/Services/DeliveryNoteService.cs | 168 +++++++++++++++++- Models/Models/PhSDeliveryNoteDetail.cs | 2 +- .../Models/PhronCareOperationsHubContext.cs | 4 +- .../Repositories/PhSDeliveryNoteRepository.cs | 2 +- .../Sales/DeliveryNoteController.cs | 26 +++ 6 files changed, 201 insertions(+), 6 deletions(-) diff --git a/Core/Interfaces/IDeliveryNoteDom.cs b/Core/Interfaces/IDeliveryNoteDom.cs index bbeff1e..fb9f44f 100644 --- a/Core/Interfaces/IDeliveryNoteDom.cs +++ b/Core/Interfaces/IDeliveryNoteDom.cs @@ -22,6 +22,11 @@ public interface IDeliveryNoteDom int page = 1, int pageSize = 50); + /// + /// Exporta a Excel los Delivery Notes filtrados. + /// + Task ExportFilteredToExcelAsync(DeliveryNoteSearchParams searchParams); + /// /// Obtiene un Delivery Note por su identificador Ășnico. /// diff --git a/Core/Services/DeliveryNoteService.cs b/Core/Services/DeliveryNoteService.cs index e5e45b8..2915301 100644 --- a/Core/Services/DeliveryNoteService.cs +++ b/Core/Services/DeliveryNoteService.cs @@ -1,8 +1,10 @@ -using Core.Interfaces; using Domain.Dtos.Sales; using Domain.Entities; using Domain.Generics; using Models.Interfaces; +using System.Reflection; +using System.Text.Json; +using Transversal.Services; namespace Core.Services { @@ -46,6 +48,62 @@ namespace Core.Services return _deliveryNoteRepository.GetDtoByIdAsync(id); } + public async Task ExportFilteredToExcelAsync(DeliveryNoteSearchParams searchParams) + { + ArgumentNullException.ThrowIfNull(searchParams); + + try + { + var searchResult = await _deliveryNoteRepository.SearchAsync( + searchParams.CustomerId, + string.IsNullOrWhiteSpace(searchParams.CustomerText) ? null : searchParams.CustomerText.Trim(), + string.IsNullOrWhiteSpace(searchParams.DeliveryNoteNumber) ? null : searchParams.DeliveryNoteNumber.Trim(), + searchParams.QuoteId, + string.IsNullOrWhiteSpace(searchParams.QuoteNumber) ? null : searchParams.QuoteNumber.Trim(), + searchParams.IssueDateFrom, + searchParams.IssueDateTo, + string.IsNullOrWhiteSpace(searchParams.Status) ? null : searchParams.Status.Trim(), + searchParams.Page <= 0 ? 1 : searchParams.Page, + searchParams.PageSize <= 0 ? 50 : searchParams.PageSize); + + if (searchResult?.Items is null || !searchResult.Items.Any()) + throw new Exception("No se encontraron remitos para exportar."); + + var items = searchResult.Items.ToList(); + var exportRows = new List(items.Count); + + foreach (var deliveryNote in items) + { + var dto = await _deliveryNoteRepository.GetDtoByIdAsync(deliveryNote.Id); + var snapshot = DeliveryNoteSnapshotInfo.FromJson(dto?.ExtraInfoJson); + + exportRows.Add(new DeliveryNoteExcelRow + { + DeliveryNoteNumber = deliveryNote.DeliveryNoteNumber, + IssueDate = deliveryNote.IssueDate.ToString("dd/MM/yyyy"), + QuoteNumber = deliveryNote.QuoteNumber, + CustomerName = deliveryNote.CustomerName, + Status = deliveryNote.Status, + ProfessionalName = snapshot.ProfessionalName, + InstitutionName = snapshot.InstitutionName, + PatientName = snapshot.PatientName, + SurgeryDate = snapshot.SurgeryDate, + Observations = deliveryNote.Observations, + PrintCount = deliveryNote.PrintCount, + CreatedAt = deliveryNote.CreatedAt.ToString("dd/MM/yyyy HH:mm") + }); + } + + var stream = new XLSXExportBase(); + return stream.ExportExcel(exportRows); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + throw new Exception($"{methodName} Message: {ex.Message}", ex); + } + } + public Task GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber) { if (string.IsNullOrWhiteSpace(deliveryNoteNumber)) @@ -125,5 +183,113 @@ namespace Core.Services DeliveryNoteNumber = created.Deliverynotenumber }; } + + private sealed class DeliveryNoteExcelRow + { + public string DeliveryNoteNumber { get; set; } = string.Empty; + public string IssueDate { get; set; } = string.Empty; + public string? QuoteNumber { get; set; } + public string? CustomerName { get; set; } + public string? Status { get; set; } + public string? ProfessionalName { get; set; } + public string? InstitutionName { get; set; } + public string? PatientName { get; set; } + public string? SurgeryDate { get; set; } + public string? Observations { get; set; } + public int PrintCount { get; set; } + public string CreatedAt { get; set; } = string.Empty; + } + + private sealed class DeliveryNoteSnapshotInfo + { + public string? ProfessionalName { get; private set; } + public string? InstitutionName { get; private set; } + public string? PatientName { get; private set; } + public string? SurgeryDate { get; private set; } + + public static DeliveryNoteSnapshotInfo FromJson(string? extraInfoJson) + { + var snapshot = new DeliveryNoteSnapshotInfo(); + + if (string.IsNullOrWhiteSpace(extraInfoJson)) + return snapshot; + + try + { + using var document = JsonDocument.Parse(extraInfoJson); + var root = document.RootElement; + + snapshot.ProfessionalName = ReadString(root, "professional", "professionalName", "doctor", "doctorName", "medico", "medicoNombre"); + snapshot.InstitutionName = ReadString(root, "institution", "institutionName", "hospital", "hospitalName", "institucion", "institucionNombre"); + snapshot.PatientName = ReadString(root, "patient", "patientName", "paciente", "pacienteNombre"); + snapshot.SurgeryDate = ReadDate(root, "surgeryDate", "estimatedDate", "fechaCirugia", "surgery_date", "estimated_date"); + } + catch + { + return snapshot; + } + + return snapshot; + } + + private static string? ReadString(JsonElement root, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + if (!TryGetPropertyIgnoreCase(root, propertyName, out var value)) + continue; + + if (value.ValueKind == JsonValueKind.String) + return value.GetString(); + + if (value.ValueKind != JsonValueKind.Null && value.ValueKind != JsonValueKind.Undefined) + return value.ToString(); + } + + return null; + } + + private static string? ReadDate(JsonElement root, params string[] propertyNames) + { + foreach (var propertyName in propertyNames) + { + if (!TryGetPropertyIgnoreCase(root, propertyName, out var value)) + continue; + + if (value.ValueKind == JsonValueKind.String) + { + var raw = value.GetString(); + + if (string.IsNullOrWhiteSpace(raw)) + return null; + + if (DateTime.TryParse(raw, out var parsedDate)) + return parsedDate.ToString("dd/MM/yyyy"); + + return raw; + } + + if (value.ValueKind != JsonValueKind.Null && value.ValueKind != JsonValueKind.Undefined) + return value.ToString(); + } + + return null; + } + + private static bool TryGetPropertyIgnoreCase(JsonElement element, string propertyName, out JsonElement value) + { + foreach (var property in element.EnumerateObject()) + { + if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + value = property.Value; + return true; + } + } + + value = default; + return false; + } + } } } diff --git a/Models/Models/PhSDeliveryNoteDetail.cs b/Models/Models/PhSDeliveryNoteDetail.cs index 667eef9..eb7c94b 100644 --- a/Models/Models/PhSDeliveryNoteDetail.cs +++ b/Models/Models/PhSDeliveryNoteDetail.cs @@ -17,7 +17,7 @@ public partial class PhSDeliveryNoteDetail public int? QuoteDetailId { get; set; } - public string Description { get; set; } = null!; + public string? Description { get; set; } public decimal Quantity { get; set; } diff --git a/Models/Models/PhronCareOperationsHubContext.cs b/Models/Models/PhronCareOperationsHubContext.cs index b2514c6..9cab453 100644 --- a/Models/Models/PhronCareOperationsHubContext.cs +++ b/Models/Models/PhronCareOperationsHubContext.cs @@ -1104,9 +1104,7 @@ public partial class PhronCareOperationsHubContext : DbContext .HasColumnType("datetime") .HasColumnName("createdat"); entity.Property(e => e.DeliverynoteId).HasColumnName("deliverynote_id"); - entity.Property(e => e.Description) - .HasMaxLength(500) - .HasColumnName("description"); + entity.Property(e => e.Description).HasColumnName("description"); entity.Property(e => e.LineNumber).HasColumnName("line_number"); entity.Property(e => e.Modifiedat) .HasColumnType("datetime") diff --git a/Models/Repositories/PhSDeliveryNoteRepository.cs b/Models/Repositories/PhSDeliveryNoteRepository.cs index 0686bec..7102e7c 100644 --- a/Models/Repositories/PhSDeliveryNoteRepository.cs +++ b/Models/Repositories/PhSDeliveryNoteRepository.cs @@ -176,7 +176,7 @@ namespace Models.Repositories OriginType = source.OriginType, OriginId = source.OriginId, QuoteDetailId = source.QuoteDetailId, - Description = source.Description, + Description = source.Description??string.Empty, Quantity = source.Quantity, Notes = source.Notes, Createdat = source.Createdat, diff --git a/phronCare.API/Controllers/Sales/DeliveryNoteController.cs b/phronCare.API/Controllers/Sales/DeliveryNoteController.cs index 86de3ff..639ba6c 100644 --- a/phronCare.API/Controllers/Sales/DeliveryNoteController.cs +++ b/phronCare.API/Controllers/Sales/DeliveryNoteController.cs @@ -150,5 +150,31 @@ namespace phronCare.API.Controllers.Sales return StatusCode(500, $"{methodName} Message: {ex.Message}"); } } + + [HttpPost("exportfiltered")] + public async Task ExportFiltered([FromBody] DeliveryNoteSearchParams searchParams) + { + try + { + var file = await _deliveryNoteService.ExportFilteredToExcelAsync(searchParams); + return File( + file, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Remitos.xlsx"); + } + catch (ArgumentException ex) + { + return BadRequest(ex.Message); + } + catch (InvalidOperationException ex) + { + return BadRequest(ex.Message); + } + catch (Exception ex) + { + var methodName = MethodBase.GetCurrentMethod()?.Name ?? "UnknownMethod"; + return BadRequest($"{methodName} Message: {ex.Message}"); + } + } } } -- 2.47.1