Compare commits

...

2 Commits

Author SHA1 Message Date
569005ec94 Merge pull request 'feat(deliverynote): add excel export with clinical snapshot parsing' (#47) from feature/leandro/46-deliverynote-excel-export into master
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (push) Successful in 2m13s
Reviewed-on: http://saludlab.com.ar:3000/leandro/phronCare/pulls/47
2026-03-27 19:04:21 +00:00
8f81614922 feat(deliverynote): add excel export with clinical snapshot parsing
All checks were successful
CI/CD Pipeline / Build and Deploy with Docker Compose (pull_request) Successful in 6m55s
- 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
2026-03-27 16:03:40 -03:00
6 changed files with 201 additions and 6 deletions

View File

@ -22,6 +22,11 @@ public interface IDeliveryNoteDom
int page = 1,
int pageSize = 50);
/// <summary>
/// Exporta a Excel los Delivery Notes filtrados.
/// </summary>
Task<byte[]> ExportFilteredToExcelAsync(DeliveryNoteSearchParams searchParams);
/// <summary>
/// Obtiene un Delivery Note por su identificador único.
/// </summary>

View File

@ -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<byte[]> 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<DeliveryNoteExcelRow>(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<DeliveryNoteDto?> 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;
}
}
}
}

View File

@ -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; }

View File

@ -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")

View File

@ -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,

View File

@ -150,5 +150,31 @@ namespace phronCare.API.Controllers.Sales
return StatusCode(500, $"{methodName} Message: {ex.Message}");
}
}
[HttpPost("exportfiltered")]
public async Task<IActionResult> 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}");
}
}
}
}