diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 977e2229..d11af6ab 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -54,7 +54,10 @@ "Bash(rm -f H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/EkaerMappingOptions.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/IShippingToEkaerMapper.cs H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer/ShippingToEkaerMapper.cs)", "Bash(rmdir \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Services/Ekaer\")", "Bash(rm -f \"H:/Applications/Mango/Source/NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/Infrastructure/NavCredentials.cs\" *)", - "Bash(rm -f \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Entities/IPostalParty.cs\" *)" + "Bash(rm -f \"H:/Applications/Aycode/Source/AyCode.Core/AyCode.Entities/IPostalParty.cs\" *)", + "PowerShell(winget install Microsoft.DotNet.Runtime.9 --architecture x64 --accept-source-agreements --accept-package-agreements)", + "PowerShell($url = \"https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.17/dotnet-runtime-9.0.17-win-x64.exe\"; $dst = \"$env:TEMP\\\\dotnet-runtime-9.0.17-win-x64.exe\"; Invoke-WebRequest -Uri $url -OutFile $dst; Get-Item $dst | Select-Object Name, Length)", + "PowerShell($p = Start-Process \"$env:TEMP\\\\dotnet-runtime-9.0.17-win-x64.exe\" -ArgumentList \"/install\",\"/quiet\",\"/norestart\" -Wait -PassThru; \"ExitCode: $\\($p.ExitCode\\)\"; dotnet --list-runtimes | Select-String \"9\\\\.0\")" ] } } diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs index ed3920fd..3b319d5f 100644 --- a/FruitBank.Common/Entities/EkaerHistory.cs +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -7,19 +7,52 @@ using Mango.Nop.Core.Entities; namespace FruitBank.Common.Entities; [AcBinarySerializable(false, true, false, true, false, false)] -//[ToonDescription("Business partner with address and tax information", Purpose = "Represents an external legal entity, specifically a Supplier who provides goods or a business partner involved in the procurement chain")] +[ToonDescription("NAV EKÁER declaration lifecycle record", Purpose = "Work-queue and audit row for one EKÁER road-freight declaration — one row per tradeCard: an incoming ShippingDocument (delivery note) or a completed outgoing Order. Tracks the declaration through generation, validation and submission to the Hungarian tax authority (NAV).")] [Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)] public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo { + [ToonDescription(Purpose = "Id of the source entity the declaration belongs to: ShippingDocument.Id when IsOutgoing is false (one declaration per delivery note, matching NAV's one-TCN-per-tradeCard granularity), Order id when IsOutgoing is true.")] public int ForeignKey { get; set; } + + [ToonDescription(Purpose = "Direction of the goods movement: false = incoming shipment (Shipping), true = outgoing delivery (Order).")] public bool IsOutgoing { get; set; } + [ToonDescription(Purpose = "Lifecycle state of the declaration: Pending (auto-created, not yet generated), Generated (tradeCard XML produced and valid), ValidationError (generation produced errors, see ErrorText), Sent (accepted by NAV, EkaerNumber filled), SendError (NAV call failed, see ErrorText).")] + public EkaerStatus Status { get; set; } + + [ToonDescription(Purpose = "The generated NAV EKÁER tradeCard request XML exactly as it was (or will be) submitted — audit copy and source of the read-only detail view. Null until the first Generate.")] + public string? XmlDoc { get; set; } + + [ToonDescription(Purpose = "The EKÁER number (TCN) assigned by NAV after a successful submission. Null until the declaration is accepted.")] + public string? EkaerNumber { get; set; } + + [ToonDescription(Purpose = "When the declaration was successfully submitted to NAV. Null until sent.")] + public DateTime? SentDate { get; set; } + + [ToonDescription(Purpose = "Validation or NAV submission error details for the ValidationError / SendError states. Null when the last operation succeeded.")] + public string? ErrorText { get; set; } + public DateTime Created { get; set; } public DateTime Modified { get; set; } } +/// Az EKÁER-bejelentés életciklus-állapota. Append-only: új érték a végére, meglévő értéke nem változhat (DB-ben int-ként tárolt). +public enum EkaerStatus +{ + /// Automatikusan létrejött, még nem volt Generate. + Pending = 0, + /// A tradeCard XML legenerálva és valid — küldhető. + Generated = 1, + /// A Generate validációs hibákkal zárult (ErrorText) — a forrásadat javítandó. + ValidationError = 2, + /// NAV által befogadva (EkaerNumber + SentDate töltve). + Sent = 3, + /// A NAV-hívás hibával zárult (ErrorText) — újraküldhető. + SendError = 4, +} + //public sealed class EkaerHistoryShipping : EkaerHistoryBase //{ // public int ShippingId diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index ac6d0730..30c81db2 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -28,6 +28,14 @@ public interface IFruitBankDataControllerCommon public Task UpdatePartnerDepot(PartnerDepot partnerDepot); #endregion PartnerDepot + #region EkaerHistory + public Task?> GetEkaerHistories(); + public Task GetEkaerHistoryById(int id); + public Task?> GetEkaerHistoriesByForeignKey(int foreignKey); + public Task AddEkaerHistory(EkaerHistory ekaerHistory); + public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); + #endregion EkaerHistory + #region CargoPartner public Task?> GetCargoPartners(); public Task GetCargoPartnerById(int id); diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor new file mode 100644 index 00000000..c01fffe2 --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor @@ -0,0 +1,86 @@ +@using AyCode.Blazor.Components.Components.Grids +@using AyCode.Core.Helpers +@using AyCode.Core.Interfaces +@using AyCode.Core.Loggers +@using AyCode.Services.Nav.Ekaer.Models +@using FruitBankHybrid.Shared.Services.Loggers +@using FruitBankHybrid.Shared.Services.SignalRs + +@inject IEnumerable LogWriters +@inject FruitBankSignalRClient FruitBankSignalRClient + +@if (TradeCard != null) +{ + @* Fejléc: csak a user-nek fontos, a mapper által ténylegesen töltött mezők. *@ +
+ Irány:@TradeCard.TradeType + Fuvarozó:@TradeCard.CarrierText + + Eladó:@FormatParty(TradeCard.SellerName, TradeCard.SellerVatNumber, TradeCard.SellerAddress) + Címzett:@FormatParty(TradeCard.DestinationName, TradeCard.DestinationVatNumber, TradeCard.DestinationAddress) + + Felrakodás:@FormatLocation(TradeCard.LoadLocation) + Lerakodás:@FormatLocation(TradeCard.UnloadLocation) + + Vontató:@TradeCard.Vehicle?.PlateNumber + Pótkocsi:@TradeCard.Vehicle2?.PlateNumber +
+} + + + + + + + + + + + + + + + + +@code { + [Parameter] public TradeCardType? TradeCard { get; set; } + [Parameter] public IId? ParentDataItem { get; set; } + + public GridEkaerDetailBase Grid { get; set; } + + public bool IsMasterGrid => ParentDataItem == null; + string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty; + + private AcObservableCollection TradeCardItems = []; + + private LoggerClient _logger; + + protected override void OnInitialized() + { + _logger = new LoggerClient(LogWriters.ToArray()); + } + + protected override void OnParametersSet() + { + TradeCardItems = TradeCard?.Items is { } items ? new AcObservableCollection(items) : []; + } + + private static string FormatParty(string? name, string? vatNumber, string? address) + { + var party = string.IsNullOrWhiteSpace(vatNumber) ? name : $"{name} ({vatNumber})"; + return string.IsNullOrWhiteSpace(address) ? party ?? string.Empty : $"{party} — {address}"; + } + + private static string FormatLocation(LocationType? location) + { + if (location == null) return string.Empty; + return string.Join(" ", new[] { location.ZipCode, location.City, location.Street }.Where(p => !string.IsNullOrWhiteSpace(p))); + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetailBase.cs b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetailBase.cs new file mode 100644 index 00000000..44171b4f --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetailBase.cs @@ -0,0 +1,19 @@ +using AyCode.Services.Nav.Ekaer.Models; +using DevExpress.Blazor; +using FruitBankHybrid.Shared.Pages; +using Microsoft.AspNetCore.Components; + +namespace FruitBankHybrid.Shared.Components.Grids.Ekaers; + +/// +/// Readonly tétel-grid az EKÁER detail row-hoz: a generált tradeCard tételeit () +/// mutatja, az EkaerHistory.XmlDoc-ból lokálisan deszerializálva. SZÁNDÉKOSAN nincs Get/Add/Update/Remove +/// tag — tag nélkül a grid readonly, a DataSource pedig lokális (nem SignalR-ből töltött). +/// A az IId<int>-et explicit partial-lal teljesíti (Id = ItemExternalId-ból számítva). +/// +public class GridEkaerDetailBase : FruitBankGridBase, IGrid +{ + public GridEkaerDetailBase() : base() + { + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor new file mode 100644 index 00000000..ed881786 --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -0,0 +1,113 @@ +@using AyCode.Blazor.Components.Components.Grids +@using AyCode.Core.Helpers +@using AyCode.Core.Interfaces +@using AyCode.Core.Loggers +@using AyCode.Services.Nav +@using AyCode.Services.Nav.Ekaer.Models +@using AyCode.Utils.Extensions +@using FruitBank.Common.Entities +@using FruitBankHybrid.Shared.Databases +@using FruitBankHybrid.Shared.Services.Loggers +@using FruitBankHybrid.Shared.Services.SignalRs + +@inject IEnumerable LogWriters +@inject FruitBankSignalRClient FruitBankSignalRClient + + + + + + + + + + + + + + + @{ + var ekaerHistory = (EkaerHistory)context.DataItem; + TradeCardType? tradeCard = null; + + if (!string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc)) + { + try + { + tradeCard = NavXmlHelper.Deserialize(ekaerHistory.XmlDoc); + } + catch (Exception ex) + { + _logger.Error($"EkaerHistory XmlDoc deserialize error; id: {ekaerHistory.Id}", ex); + } + } + + + } + + + @if (IsMasterGrid) + { + @* EKÁER-rekordot a rendszer hoz létre (auto-rekord) — kézi new/edit/delete tiltva. *@ + + } + + + + + +@code { + [Inject] public required DatabaseClient Database { get; set; } + + [Parameter] public AcObservableCollection? EkaerHistories { get; set; } + + const string ExportFileName = "ExportResult"; + string GridSearchText = ""; + bool EditItemsEnabled { get; set; } + int FocusedRowVisibleIndex { get; set; } + public GridEkaerHistoryBase Grid { get; set; } + string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty; + + [Parameter] public IId? ParentDataItem { get; set; } + + public bool IsMasterGrid => ParentDataItem == null; + + private LoggerClient _logger; + + protected override async Task OnInitializedAsync() + { + _logger = new LoggerClient(LogWriters.ToArray()); + await ReloadDataFromDb(false); + } + + private async Task ReloadDataFromDb(bool forceReload = false) + { + if (!IsMasterGrid) return; + + if (Grid == null) return; + + using (await ObjectLock.GetSemaphore().UseWaitAsync()) + if (forceReload) await Grid.ReloadDataSourceAsync(); + + if (forceReload) Grid.Reload(); + } + + async Task Grid_FocusedRowChanged(GridFocusedRowChangedEventArgs args) + { + if (Grid == null) return; + + if (Grid.IsEditing() && !Grid.IsEditingNewRow()) + await Grid.SaveChangesAsync(); + + FocusedRowVisibleIndex = args.VisibleIndex; + EditItemsEnabled = true; + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs new file mode 100644 index 00000000..e63824f8 --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs @@ -0,0 +1,66 @@ +using AyCode.Utils.Extensions; +using DevExpress.Blazor; +using FruitBank.Common.Entities; +using FruitBank.Common.SignalRs; +using FruitBankHybrid.Shared.Pages; +using Microsoft.AspNetCore.Components; + +namespace FruitBankHybrid.Shared.Components.Grids.Ekaers; + +public class GridEkaerHistoryBase : FruitBankGridBase, IGrid +{ + private bool _isFirstInitializeParameterCore; + private bool _isFirstInitializeParameters; + + public GridEkaerHistoryBase() : base() + { + //GetAllMessageTag = SignalRTags.GetEkaerHistories; + AddMessageTag = SignalRTags.AddEkaerHistory; + UpdateMessageTag = SignalRTags.UpdateEkaerHistory; + + //RemoveMessageTag = SignalRTags.; + } + + protected override async Task OnInitializedAsync() + { + if (GetAllMessageTag > 0) return; + + if (IsMasterGrid) GetAllMessageTag = SignalRTags.GetEkaerHistories; + else + { + if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id]; + + // A ForeignKey általános (Shipping/Order stb.) — bármely szülőnél a ForeignKey-re szűrünk. + GetAllMessageTag = SignalRTags.GetEkaerHistoriesByForeignKey; + if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(EkaerHistory.ForeignKey); + } + + await base.OnInitializedAsync(); + } + + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (!_isFirstInitializeParameters) + { + _isFirstInitializeParameters = false; + } + } + + protected override async Task SetParametersAsyncCore(ParameterView parameters) + { + await base.SetParametersAsyncCore(parameters); + + if (!_isFirstInitializeParameterCore) + { + //ShowFilterRow = true; + //ShowGroupPanel = true; + //AllowSort = false; + + //etc... + + _isFirstInitializeParameterCore = false; + } + } +} diff --git a/FruitBankHybrid.Shared/Pages/Ekaer.razor b/FruitBankHybrid.Shared/Pages/Ekaer.razor index d3443383..7f4f6534 100644 --- a/FruitBankHybrid.Shared/Pages/Ekaer.razor +++ b/FruitBankHybrid.Shared/Pages/Ekaer.razor @@ -1,5 +1,6 @@ @page "/Ekaer" @using FruitBankHybrid.Shared.Components +@using FruitBankHybrid.Shared.Components.Grids.Ekaers @using FruitBankHybrid.Shared.Databases

EKÁER - Adminisztrátor

@@ -15,10 +16,11 @@ - @* A beküldésre váró EKÁER-ek grid-je ide kerül — külön feladat (kliens-metódus, szűrés). *@ + @* TODO: "váró vs. elküldött" szűrés — az EkaerHistory-ban még nincs állapot-mező. *@ + - @* Az elküldött EKÁER-ek grid-je ide kerül — külön feladat (kliens-metódus, szűrés). *@ + diff --git a/FruitBankHybrid.Shared/Pages/Ekaer.razor.cs b/FruitBankHybrid.Shared/Pages/Ekaer.razor.cs index f3a306a4..a9861584 100644 --- a/FruitBankHybrid.Shared/Pages/Ekaer.razor.cs +++ b/FruitBankHybrid.Shared/Pages/Ekaer.razor.cs @@ -1,5 +1,6 @@ using AyCode.Core.Loggers; using FruitBank.Common.Models; +using FruitBankHybrid.Shared.Components.Grids.Ekaers; using FruitBankHybrid.Shared.Services.Loggers; using Mango.Nop.Core.Loggers; using Microsoft.AspNetCore.Components; @@ -12,6 +13,9 @@ public partial class Ekaer : ComponentBase [Inject] public required NavigationManager NavManager { get; set; } [Inject] public required LoggedInModel LoggedInModel { get; set; } + private GridEkaerHistory gridEkaerHistoryPending; + private GridEkaerHistory gridEkaerHistorySent; + private ILogger _logger = null!; public int ActiveTabIndex; diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index dd696f71..1f10aa12 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -73,6 +73,14 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task UpdatePartnerDepot(PartnerDepot partnerDepot) => PostDataAsync(SignalRTags.UpdatePartnerDepot, partnerDepot); #endregion PartnerDepot + #region EkaerHistory + public Task?> GetEkaerHistories() => GetAllAsync>(SignalRTags.GetEkaerHistories); + public Task GetEkaerHistoryById(int id) => GetByIdAsync(SignalRTags.GetEkaerHistoryById, id); + public Task?> GetEkaerHistoriesByForeignKey(int foreignKey) => GetAllAsync>(SignalRTags.GetEkaerHistoriesByForeignKey, [foreignKey]); + public Task AddEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.AddEkaerHistory, ekaerHistory); + public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory); + #endregion EkaerHistory + #region CargoPartner public Task?> GetCargoPartners() => GetAllAsync>(SignalRTags.GetCargoPartners); public Task GetCargoPartnerById(int id) => GetByIdAsync(SignalRTags.GetCargoPartnerById, id);