From 169f7fc133ea76a43b6a08876512d40649787f5a Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 10 Jul 2026 12:54:58 +0200 Subject: [PATCH] xml download --- FruitBank.Common/Entities/Preorder.cs | 17 +++++++++-- FruitBank.Common/Entities/PreorderItem.cs | 16 +++++++++-- .../Grids/Ekaers/GridEkaerHistory.razor | 28 ++++++++++++------- .../wwwroot/js/fileDownload.js | 19 +++++++++++++ FruitBankHybrid.Web/Components/App.razor | 1 + FruitBankHybrid/wwwroot/index.html | 1 + 6 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 FruitBankHybrid.Shared/wwwroot/js/fileDownload.js diff --git a/FruitBank.Common/Entities/Preorder.cs b/FruitBank.Common/Entities/Preorder.cs index cece1030..ff983c5a 100644 --- a/FruitBank.Common/Entities/Preorder.cs +++ b/FruitBank.Common/Entities/Preorder.cs @@ -21,9 +21,22 @@ public sealed class PreOrder : MgEntityBase [ToonDescription(Purpose = "Requested delivery date. Drives the conversion window (only preorders within PreOrderConversionWindowDays — 4 days — of this date are eligible for allocation) and the expiry sweep — once this date is past, any still-Pending items are Dropped.")] public DateTime DateOfReceipt { get; set; } + // Persisted as an int column ("Status"). A raw enum property is NOT mapped/written by the + // nopCommerce LinqToDB stack (nop stores enums as int + a [NotMapped] wrapper — cf. Order.OrderStatusId), + // so writing the enum directly left the column stuck at 0. StatusId is the real mapped column; + // the Status wrapper below is the typed API. + [Column("Status")] + [ToonDescription(Purpose = "Header lifecycle status, stored as int. See the Status wrapper for the enum semantics.")] + public int StatusId { get; set; } + + [NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped] [ToonDescription(Purpose = "Header lifecycle: Pending / Confirmed (all items Fulfilled) / PartiallyFulfilled (some Dropped or partial, none left Pending) / Cancelled.", - BusinessRule = "set by RefreshPreOrderStatusAsync from item states: items.All(Fulfilled) => Confirmed; (any Dropped or PartiallyFulfilled) && none Pending => PartiallyFulfilled; else Pending. Cancelled is set explicitly.")] - public PreOrderStatus Status { get; set; } + BusinessRule = "set by RefreshPreOrderStatusAsync from item states: items.All(Fulfilled) => Confirmed; (any Dropped or PartiallyFulfilled) && none Pending => PartiallyFulfilled; else Pending. Cancelled is set explicitly. Persisted via StatusId.")] + public PreOrderStatus Status + { + get => (PreOrderStatus)StatusId; + set => StatusId = (int)value; + } [ToonDescription(Purpose = "Optional free-text note entered by the customer when placing the preorder.")] public string? CustomerNote { get; set; } diff --git a/FruitBank.Common/Entities/PreorderItem.cs b/FruitBank.Common/Entities/PreorderItem.cs index ad8259e4..1b220a11 100644 --- a/FruitBank.Common/Entities/PreorderItem.cs +++ b/FruitBank.Common/Entities/PreorderItem.cs @@ -29,7 +29,19 @@ public sealed class PreOrderItem : MgEntityBase Constraints = "non-negative")] public decimal UnitPriceInclTax { get; set; } + // Persisted as an int column ("Status"). A raw enum property is NOT mapped/written by the + // nopCommerce LinqToDB stack, so writing the enum directly left the column stuck at 0. + // StatusId is the real mapped column; the Status wrapper below is the typed API. + [Column("Status")] + [ToonDescription(Purpose = "Item lifecycle status, stored as int. See the Status wrapper for the enum semantics.")] + public int StatusId { get; set; } + + [NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped] [ToonDescription(Purpose = "Item lifecycle: Pending / Fulfilled (fully allocated) / PartiallyFulfilled (partly allocated) / Dropped (expired or no incoming stock).", - BusinessRule = "set during conversion: FulfilledQuantity >= RequestedQuantity ? Fulfilled : FulfilledQuantity > 0 ? PartiallyFulfilled : Dropped; stays Pending until first allocation")] - public PreOrderItemStatus Status { get; set; } + BusinessRule = "set during conversion: FulfilledQuantity >= RequestedQuantity ? Fulfilled : FulfilledQuantity > 0 ? PartiallyFulfilled : Dropped; stays Pending until first allocation. Persisted via StatusId.")] + public PreOrderItemStatus Status + { + get => (PreOrderItemStatus)StatusId; + set => StatusId = (int)value; + } } diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index e3f7e648..21496a74 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -83,12 +83,12 @@ Enabled="@(CanGenerate(row))" Attributes="@(new Dictionary { ["title"] = row.ErrorText ?? "EKÁER XML generálása és validálása" })" Click="async () => await OnGenerateClick(row)" /> - + Enabled="@(CanDownload(row))" + Attributes="@(new Dictionary { ["title"] = CanDownload(row) ? "A generált XML letöltése fájlként (kézi NAV-beadáshoz)" : "Hibás bejelentés nem tölthető le — javítsd az adatokat és generáld újra." })" + Click="async () => await OnDownloadClick(row)" /> } @@ -215,8 +215,8 @@ private bool CanGenerate(EkaerHistory ekaerHistory) => !ekaerHistory.Status.IsSent() && !_generatingIds.Contains(ekaerHistory.Id); - // Csak BLOKKOLÓ hiba (ValidationError) esetén NEM másolható; a warning (pl. hiányzó rendszám) küldhető → másolható. - private static bool CanCopy(EkaerHistory ekaerHistory) + // Csak BLOKKOLÓ hiba (ValidationError) esetén NEM tölthető le; a warning (pl. hiányzó rendszám) küldhető → letölthető. + private static bool CanDownload(EkaerHistory ekaerHistory) => ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc); // A szülő-oldal (fül-számlálók) értesítése egy státuszt érintő művelet után. Üres delegáltnál no-op. @@ -297,17 +297,25 @@ await DialogService.ShowMessageBoxAsync("EKÁER — kihagyott tételek", string.Join(Environment.NewLine, skipped), MessageBoxRenderStyle.Warning); } - private async Task OnCopyClick(EkaerHistory ekaerHistory) + private async Task OnDownloadClick(EkaerHistory ekaerHistory) { - if (!CanCopy(ekaerHistory)) return; + if (!CanDownload(ekaerHistory)) return; + + // Fájlnév: "yyyy.MM.dd-Partner.xml" — a partnernévből az érvénytelen fájlnév-karaktereket kicseréljük. + var datePart = ekaerHistory.ShippingDate?.ToString("yyyy.MM.dd") ?? "nincs-datum"; + var partner = string.IsNullOrWhiteSpace(ekaerHistory.Partner) ? "partner" : ekaerHistory.Partner; + var invalid = System.IO.Path.GetInvalidFileNameChars(); + var safePartner = new string(partner.Select(c => invalid.Contains(c) ? '_' : c).ToArray()).Trim(); + if (string.IsNullOrWhiteSpace(safePartner)) safePartner = "partner"; + var fileName = $"{datePart}-{safePartner}.xml"; try { - await JS.InvokeVoidAsync("navigator.clipboard.writeText", ekaerHistory.XmlDoc); + await JS.InvokeVoidAsync("downloadFileFromText", fileName, ekaerHistory.XmlDoc, "application/xml"); } catch (Exception ex) { - _logger.Error($"XmlDoc vágólapra másolása sikertelen; EkaerHistory.Id: {ekaerHistory.Id}", ex); + _logger.Error($"XmlDoc letöltése sikertelen; EkaerHistory.Id: {ekaerHistory.Id}", ex); } } } diff --git a/FruitBankHybrid.Shared/wwwroot/js/fileDownload.js b/FruitBankHybrid.Shared/wwwroot/js/fileDownload.js new file mode 100644 index 00000000..cffa4f02 --- /dev/null +++ b/FruitBankHybrid.Shared/wwwroot/js/fileDownload.js @@ -0,0 +1,19 @@ +// Triggers a client-side file download from in-memory text (no server round-trip). +// Used e.g. by the EKÁER grid to save the generated trade-card XML as a file. +window.downloadFileFromText = function (filename, content, mime) { + try { + const blob = new Blob([content], { type: mime || 'application/octet-stream' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename || 'download'; + document.body.appendChild(a); + a.click(); + setTimeout(function () { + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, 0); + } catch (e) { + console.error('downloadFileFromText failed', e); + } +}; diff --git a/FruitBankHybrid.Web/Components/App.razor b/FruitBankHybrid.Web/Components/App.razor index ce740e78..3a1ef18c 100644 --- a/FruitBankHybrid.Web/Components/App.razor +++ b/FruitBankHybrid.Web/Components/App.razor @@ -33,6 +33,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; + diff --git a/FruitBankHybrid/wwwroot/index.html b/FruitBankHybrid/wwwroot/index.html index f3f0fa2c..fcb82ab1 100644 --- a/FruitBankHybrid/wwwroot/index.html +++ b/FruitBankHybrid/wwwroot/index.html @@ -33,6 +33,7 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; +