Merge branch 'FruitBank_v0.0.8.0' of https://git.aycode.com/Adam/FruitBankHybridApp into FruitBank_v0.0.8.0

This commit is contained in:
Loretta 2026-07-10 15:38:29 +02:00
commit b6dddd6f1f
6 changed files with 68 additions and 14 deletions

View File

@ -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.")] [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; } 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.", [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.")] 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; set; } public PreOrderStatus Status
{
get => (PreOrderStatus)StatusId;
set => StatusId = (int)value;
}
[ToonDescription(Purpose = "Optional free-text note entered by the customer when placing the preorder.")] [ToonDescription(Purpose = "Optional free-text note entered by the customer when placing the preorder.")]
public string? CustomerNote { get; set; } public string? CustomerNote { get; set; }

View File

@ -29,7 +29,19 @@ public sealed class PreOrderItem : MgEntityBase
Constraints = "non-negative")] Constraints = "non-negative")]
public decimal UnitPriceInclTax { get; set; } 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).", [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")] BusinessRule = "set during conversion: FulfilledQuantity >= RequestedQuantity ? Fulfilled : FulfilledQuantity > 0 ? PartiallyFulfilled : Dropped; stays Pending until first allocation. Persisted via StatusId.")]
public PreOrderItemStatus Status { get; set; } public PreOrderItemStatus Status
{
get => (PreOrderItemStatus)StatusId;
set => StatusId = (int)value;
}
} }

View File

@ -83,12 +83,12 @@
Enabled="@(CanGenerate(row))" Enabled="@(CanGenerate(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = row.ErrorText ?? "EKÁER XML generálása és validálása" })" Attributes="@(new Dictionary<string, object> { ["title"] = row.ErrorText ?? "EKÁER XML generálása és validálása" })"
Click="async () => await OnGenerateClick(row)" /> Click="async () => await OnGenerateClick(row)" />
<DxButton Text="Copy" <DxButton Text="Letöltés"
SizeMode="SizeMode.Small" SizeMode="SizeMode.Small"
RenderStyle="ButtonRenderStyle.Secondary" RenderStyle="ButtonRenderStyle.Secondary"
Enabled="@(CanCopy(row))" Enabled="@(CanDownload(row))"
Attributes="@(new Dictionary<string, object> { ["title"] = CanCopy(row) ? "A generált XML vágólapra másolása (kézi NAV-beadáshoz)" : "Hibás bejelentés nem másolható — javítsd az adatokat és generáld újra." })" Attributes="@(new Dictionary<string, object> { ["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 OnCopyClick(row)" /> Click="async () => await OnDownloadClick(row)" />
} }
</CellDisplayTemplate> </CellDisplayTemplate>
</DxGridDataColumn> </DxGridDataColumn>
@ -215,8 +215,8 @@
private bool CanGenerate(EkaerHistory ekaerHistory) private bool CanGenerate(EkaerHistory ekaerHistory)
=> !ekaerHistory.Status.IsSent() && !_generatingIds.Contains(ekaerHistory.Id); => !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ó. // Csak BLOKKOLÓ hiba (ValidationError) esetén NEM tölthető le; a warning (pl. hiányzó rendszám) küldhető → letölthető.
private static bool CanCopy(EkaerHistory ekaerHistory) private static bool CanDownload(EkaerHistory ekaerHistory)
=> ekaerHistory.Status.IsSubmittable() && !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc); => 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. // 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); 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 try
{ {
await JS.InvokeVoidAsync("navigator.clipboard.writeText", ekaerHistory.XmlDoc); await JS.InvokeVoidAsync("downloadFileFromText", fileName, ekaerHistory.XmlDoc, "application/xml");
} }
catch (Exception ex) 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);
} }
} }
} }

View File

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

View File

@ -33,6 +33,7 @@
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script> </script>
<script src="_content/FruitBankHybrid.Shared/js/pdfViewer.js"></script> <script src="_content/FruitBankHybrid.Shared/js/pdfViewer.js"></script>
<script src="_content/FruitBankHybrid.Shared/js/fileDownload.js"></script>
<script src="@Assets["/_framework/blazor.web.js"]"></script> <script src="@Assets["/_framework/blazor.web.js"]"></script>
</body> </body>

View File

@ -33,6 +33,7 @@
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
</script> </script>
<script src="_content/FruitBankHybrid.Shared/js/pdfViewer.js"></script> <script src="_content/FruitBankHybrid.Shared/js/pdfViewer.js"></script>
<script src="_content/FruitBankHybrid.Shared/js/fileDownload.js"></script>
</body> </body>