diff --git a/Documents/Models/DocumentType.cs b/Documents/Models/DocumentType.cs index e9a06f8..bbd0582 100644 --- a/Documents/Models/DocumentType.cs +++ b/Documents/Models/DocumentType.cs @@ -4,9 +4,9 @@ { Quote, Expedition, + DeliveryNote, Invoice, Order, - Remito, Certificate } } diff --git a/Documents/Services/DocumentTemplateService.cs b/Documents/Services/DocumentTemplateService.cs index 8823c89..f5c8213 100644 --- a/Documents/Services/DocumentTemplateService.cs +++ b/Documents/Services/DocumentTemplateService.cs @@ -2,6 +2,7 @@ using Documents.Interfaces; using Documents.Models; using Domain.Dtos; // QuoteDto +using Domain.Dtos.Sales; // DeliveryNoteDto using Domain.Dtos.Stock; // ExpeditionDto using Transversal.Interfaces; @@ -57,6 +58,7 @@ public class DocumentTemplateService : IDocumentTemplateService private static string ResolveTemplate(DocumentType type) => type switch { DocumentType.Quote => "Quotes/Template_v1.cshtml", + DocumentType.DeliveryNote => "DeliveryNotes/Template_v1.cshtml", DocumentType.Expedition => "Expeditions/Template_v1.cshtml", _ => "Shared/Template_Generic.cshtml" }; @@ -72,6 +74,9 @@ public class DocumentTemplateService : IDocumentTemplateService case ExpeditionDto e: e.LogoBase64 = base64; break; + case DeliveryNoteDto d: + d.LogoBase64 = base64; + break; default: // Si no tiene LogoBase64, no hacemos nada. break; diff --git a/Documents/Templates/DeliveryNotes/Template_v1.cshtml b/Documents/Templates/DeliveryNotes/Template_v1.cshtml new file mode 100644 index 0000000..a5cae79 --- /dev/null +++ b/Documents/Templates/DeliveryNotes/Template_v1.cshtml @@ -0,0 +1,182 @@ +@using System +@using System.Globalization +@using System.Text.Json +@using Domain.Dtos.Sales +@model DeliveryNoteDto + +@{ + Layout = null; + + var ci = CultureInfo.GetCultureInfo("es-AR"); + CultureInfo.CurrentCulture = ci; + CultureInfo.CurrentUICulture = ci; + + SurgerySnapshot snap; + if (string.IsNullOrWhiteSpace(Model.ExtraInfoJson)) + { + snap = new SurgerySnapshot(); + } + else + { + try + { + snap = JsonSerializer.Deserialize(Model.ExtraInfoJson) ?? new SurgerySnapshot(); + } + catch + { + snap = new SurgerySnapshot(); + } + } + + var reprintText = Model.PrintCount > 0 ? (" — Reimpresión " + Model.PrintCount) : string.Empty; +} + +@functions { + public class SurgerySnapshot + { + public string? Professional { get; set; } + public string? Institution { get; set; } + public string? Patient { get; set; } + public DateTime? SurgeryDate { get; set; } + } + + public static string FQty(decimal q) => q.ToString("G29", CultureInfo.InvariantCulture); + public static string FDate(DateTime? d) => d.HasValue ? d.Value.ToString("dd/MM/yyyy") : string.Empty; + public static string FText(string? value) => string.IsNullOrWhiteSpace(value) ? "-" : value.Trim(); + public static string FOrigin(byte originType) => originType switch + { + 1 => "Presupuesto", + 2 => "Manual", + _ => originType.ToString() + }; +} + + + + + + Remito @Model.DeliveryNoteNumber + + + + +
+
+
+ @if (!string.IsNullOrWhiteSpace(Model.LogoBase64)) + { + Logo + } +
Documento generado por PhronCare
+
+
+

Remito

+
@Model.DeliveryNoteNumber@reprintText
+
Fecha: @Model.IssueDate.ToString("dd/MM/yyyy")
+
+
+ +
+ +
+ + + + + + + + + + + + + +
Cliente@FText(Model.CustomerName)Estado@FText(Model.Status)
Presupuesto@FText(Model.QuoteNumber)ID interno@Model.Id
+
+ +
Contexto clínico
+
+ + + + + + + + + + + + + +
Profesional@FText(snap.Professional)Institución@FText(snap.Institution)
Paciente@FText(snap.Patient)Fecha cirugía@FDate(snap.SurgeryDate)
+
+ +
Detalle de ítems
+
+ + + + + + + + + + + + + @foreach (var item in Model.Items.OrderBy(i => i.LineNumber)) + { + + + + + + + + + } + +
#DescripciónCantidadOrigenRef.Notas
@item.LineNumber@FText(item.Description)@FQty(item.Quantity)@FOrigin(item.OriginType)@(item.OriginId?.ToString() ?? "-")@FText(item.Notes)
+
+ +
Observaciones
+
@FText(Model.Observations)
+ + +
+ + diff --git a/Domain/Dtos/Sales/DeliveryNoteDto.cs b/Domain/Dtos/Sales/DeliveryNoteDto.cs index 67e76a9..7ada9c7 100644 --- a/Domain/Dtos/Sales/DeliveryNoteDto.cs +++ b/Domain/Dtos/Sales/DeliveryNoteDto.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; namespace Domain.Dtos.Sales @@ -11,11 +11,14 @@ namespace Domain.Dtos.Sales { public int Id { get; set; } public string DeliveryNoteNumber { get; set; } = string.Empty; + public string CustomerName { get; set; } = string.Empty; public int? QuoteId { get; set; } + public string? QuoteNumber { get; set; } public int? SalesInvoiceId { get; set; } public DateTime IssueDate { get; set; } public int CustomerId { get; set; } public string Status { get; set; } = string.Empty; + public string? LogoBase64 { get; set; } public string? Observations { get; set; } public string? ExtraInfoJson { get; set; } public int PrintCount { get; set; } diff --git a/Models/Repositories/PhSDeliveryNoteRepository.cs b/Models/Repositories/PhSDeliveryNoteRepository.cs index c4d5928..0686bec 100644 --- a/Models/Repositories/PhSDeliveryNoteRepository.cs +++ b/Models/Repositories/PhSDeliveryNoteRepository.cs @@ -86,6 +86,8 @@ namespace Models.Repositories public async Task GetDtoByIdAsync(int id) { var entity = await _context.PhSDeliveryNotes + .Include(x => x.Customer) + .Include(x => x.Quote) .Include(x => x.PhSDeliveryNoteDetails) .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id); @@ -96,6 +98,8 @@ namespace Models.Repositories public async Task GetDtoByDeliveryNoteNumberAsync(string deliveryNoteNumber) { var entity = await _context.PhSDeliveryNotes + .Include(x => x.Customer) + .Include(x => x.Quote) .Include(x => x.PhSDeliveryNoteDetails) .AsNoTracking() .FirstOrDefaultAsync(x => x.Deliverynotenumber == deliveryNoteNumber); @@ -106,6 +110,8 @@ namespace Models.Repositories public async Task> GetDtosByQuoteIdAsync(int quoteId) { var entities = await _context.PhSDeliveryNotes + .Include(x => x.Customer) + .Include(x => x.Quote) .Include(x => x.PhSDeliveryNoteDetails) .AsNoTracking() .Where(x => x.QuoteId == quoteId) @@ -139,7 +145,9 @@ namespace Models.Repositories { Id = source.Id, DeliveryNoteNumber = source.Deliverynotenumber, + CustomerName = source.Customer?.Name ?? string.Empty, QuoteId = source.QuoteId, + QuoteNumber = source.Quote?.Quotenumber, SalesInvoiceId = source.SalesinvoiceId, IssueDate = source.Issuedate, CustomerId = source.CustomerId, @@ -147,6 +155,7 @@ namespace Models.Repositories Observations = source.Observations, ExtraInfoJson = source.ExtrainfoJson, PrintCount = source.Printcount, + LogoBase64 = null, CreatedAt = source.Createdat, ModifiedAt = source.Modifiedat, Items = source.PhSDeliveryNoteDetails diff --git a/phronCare.API/Controllers/Sales/DeliveryNoteController.cs b/phronCare.API/Controllers/Sales/DeliveryNoteController.cs index c25215c..86de3ff 100644 --- a/phronCare.API/Controllers/Sales/DeliveryNoteController.cs +++ b/phronCare.API/Controllers/Sales/DeliveryNoteController.cs @@ -1,4 +1,6 @@ -using Core.Interfaces; +using Core.Interfaces; +using Documents.Interfaces; +using Documents.Models; using Domain.Dtos.Sales; using Domain.Generics; using Microsoft.AspNetCore.Mvc; @@ -10,10 +12,14 @@ namespace phronCare.API.Controllers.Sales [ApiController] public class DeliveryNoteController : ControllerBase { + private readonly IDocumentTemplateService _documentTemplateService; private readonly IDeliveryNoteDom _deliveryNoteService; - public DeliveryNoteController(IDeliveryNoteDom deliveryNoteService) + public DeliveryNoteController( + IDocumentTemplateService documentTemplateService, + IDeliveryNoteDom deliveryNoteService) { + _documentTemplateService = documentTemplateService ?? throw new ArgumentNullException(nameof(documentTemplateService)); _deliveryNoteService = deliveryNoteService ?? throw new ArgumentNullException(nameof(deliveryNoteService)); } @@ -89,6 +95,24 @@ namespace phronCare.API.Controllers.Sales } } + + [HttpGet("{id:int}/pdf")] + public async Task GetDeliveryNotePdf(int id) + { + var deliveryNote = await _deliveryNoteService.GetDtoByIdAsync(id); + + if (deliveryNote == null) + return NotFound($"Remito con ID {id} no encontrado."); + + var pdfBytes = await _documentTemplateService.GenerateDocumentAsync(new DocumentGenerationRequest + { + Model = deliveryNote, + DocumentType = DocumentType.DeliveryNote + }); + + return File(pdfBytes, "application/pdf", $"Remito_{deliveryNote.DeliveryNoteNumber}.pdf"); + } + [HttpGet("by-quote/{quoteId:int}")] public async Task>> GetByQuoteId(int quoteId) { diff --git a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor index 7d4d006..60d5fd1 100644 --- a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor +++ b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNoteCreate.razor @@ -369,6 +369,7 @@ var response = await DeliveryNoteService.CreateAndIssueAsync(request); toastService.ShowSuccess($"Remito {response.DeliveryNoteNumber} emitido correctamente."); + await DeliveryNoteService.ExportPdfAsync(response.Id, response.DeliveryNoteNumber); Navigation.NavigateTo("/deliverynotes"); } catch (Exception ex) diff --git a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor index 31e333e..68f04e0 100644 --- a/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor +++ b/phronCare.UIBlazor/Pages/Sales/DeliveryNotes/DeliveryNotes.razor @@ -93,6 +93,7 @@ @deliveryNote.PrintCount + } @@ -256,6 +257,18 @@ return Task.CompletedTask; } + private async Task PrintPdfAsync(DeliveryNoteSummaryDto deliveryNote) + { + try + { + await deliveryNoteService.ExportPdfAsync(deliveryNote.Id, deliveryNote.DeliveryNoteNumber); + } + catch (Exception ex) + { + toastService.ShowError(ex.Message); + } + } + private void ExportarExcel() { toastService.ShowInfo("La exportación a Excel se implementará en una próxima story."); diff --git a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs index c3c779f..a18a2e4 100644 --- a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/DeliveryNoteService.cs @@ -1,16 +1,19 @@ using Domain.Dtos.Sales; using Domain.Generics; +using Microsoft.JSInterop; using System.Net.Http.Json; namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes { public class DeliveryNoteService : IDeliveryNoteService { + private readonly IJSRuntime _js; private readonly HttpClient _http; - public DeliveryNoteService(HttpClient http) + public DeliveryNoteService(HttpClient http, IJSRuntime js) { _http = http; + _js = js; } @@ -94,5 +97,29 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes var result = await response.Content.ReadFromJsonAsync(); return result ?? throw new Exception("Respuesta vacía del servidor."); } + + public async Task ExportPdfAsync(int deliveryNoteId, string deliveryNoteNumber) + { + try + { + var response = await _http.GetAsync($"/api/deliverynote/{deliveryNoteId}/pdf"); + + if (!response.IsSuccessStatusCode) + { + var error = await response.Content.ReadAsStringAsync(); + throw new Exception($"Error al generar PDF: {error}"); + } + + var bytes = await response.Content.ReadAsByteArrayAsync(); + var base64 = Convert.ToBase64String(bytes); + var fileName = $"{deliveryNoteNumber}.pdf"; + + await _js.InvokeVoidAsync("saveAsFile", fileName, base64); + } + catch (Exception ex) + { + throw new Exception($"ExportPdfAsync: {ex.Message}", ex); + } + } } } diff --git a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs index f26b1ca..82737ec 100644 --- a/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs +++ b/phronCare.UIBlazor/Services/Sales/DeliveryNotes/IDeliveryNoteService.cs @@ -10,5 +10,6 @@ namespace phronCare.UIBlazor.Services.Sales.DeliveryNotes Task GetByDeliveryNoteNumberAsync(string deliveryNoteNumber); Task> GetByQuoteIdAsync(int quoteId); Task CreateAndIssueAsync(DeliveryNoteCreateRequest request); + Task ExportPdfAsync(int deliveryNoteId, string deliveryNoteNumber); } }