From 0083a7bd6ecdc6b2fc9dec387b9637f82c567831 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 5 Jun 2026 06:18:33 +0200 Subject: [PATCH 01/17] Add PartnerDepot and EkaerHistory entities, update tags - Introduced PartnerDepot and EkaerHistory entity classes with serialization and DB mapping. - Made ICompanyInfoBase.TaxId nullable. - Added new DB table name constants for PartnerDepot and EkaerHistory. - Added SignalR tag constants for PartnerDepot and EkaerHistory operations. - Updated SQL schema compare connection strings and item order. --- FruitBank.Common/Entities/EkaerHistory.cs | 58 +++++++++++++++++++++++ FruitBank.Common/Entities/Partner.cs | 1 - FruitBank.Common/Entities/PartnerDepot.cs | 38 +++++++++++++++ FruitBank.Common/FruitBankConstClient.cs | 3 ++ FruitBank.Common/SignalRs/SignalRTags.cs | 11 +++++ SqlSchemaCompare_Dev_to_Prod.scmp | 18 +++---- 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 FruitBank.Common/Entities/EkaerHistory.cs create mode 100644 FruitBank.Common/Entities/PartnerDepot.cs diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs new file mode 100644 index 00000000..70fba14a --- /dev/null +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -0,0 +1,58 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using AyCode.Interfaces.TimeStampInfo; +using LinqToDB.Mapping; +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")] +[Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)] + +public abstract class EkaerHistory: MgEntityBase, ITimeStampInfo +{ + public int ForeignKey { get; set; } + public bool IsOutgoing { get; set; } + + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} + +//public sealed class EkaerHistoryShipping : EkaerHistoryBase +//{ +// public int ShippingId +// { +// get => ForeignItemId; +// set => ForeignItemId = value; +// } + +// [Association(ThisKey = nameof(ShippingId), OtherKey = nameof(Shipping.Id), CanBeNull = true)] +// public Shipping? Shipping { get; set; } +//} + +//public sealed class EkaerHistoryOrder : EkaerHistoryBase +//{ +// public int ShippingId +// { +// get => ForeignItemId; +// set => ForeignItemId = value; +// } + +// [Association(ThisKey = nameof(ShippingId), OtherKey = nameof(Shipping.Id), CanBeNull = true)] +// public Shipping? Shipping { get; set; } +//} + +//public abstract class EkaerHistoryBase : MgEntityBase, ITimeStampInfo +//{ +// [NotColumn] +// protected int ForeignItemId; + +// [NotColumn] +// [ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")] +// public int ForeignKey => ForeignItemId; + +// public DateTime Created { get; set; } +// public DateTime Modified { get; set; } +//} \ No newline at end of file diff --git a/FruitBank.Common/Entities/Partner.cs b/FruitBank.Common/Entities/Partner.cs index f4793dee..0acd2457 100644 --- a/FruitBank.Common/Entities/Partner.cs +++ b/FruitBank.Common/Entities/Partner.cs @@ -2,7 +2,6 @@ using AyCode.Core.Serializers.Toons; using FruitBank.Common.Interfaces; using LinqToDB.Mapping; -using Mango.Nop.Core.Entities; namespace FruitBank.Common.Entities; diff --git a/FruitBank.Common/Entities/PartnerDepot.cs b/FruitBank.Common/Entities/PartnerDepot.cs new file mode 100644 index 00000000..94f91a41 --- /dev/null +++ b/FruitBank.Common/Entities/PartnerDepot.cs @@ -0,0 +1,38 @@ +using AyCode.Core.Interfaces; +using AyCode.Core.Serializers.Attributes; +using AyCode.Interfaces.TimeStampInfo; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +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")] +[LinqToDB.Mapping.Table(Name = FruitBankConstClient.PartnerDepotDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDepotDbTableName)] +public sealed class PartnerDepot : MgEntityBase, ITimeStampInfo, ICompanyInfoBase +{ + public int PartnerId { get; set; } + + public string Name { get; set;} + + [NotColumn, NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore] + public string? TaxId => Partner?.TaxId; + + public string CountryCode { get; set; } + public string PostalCode { get; set;} + public string City { get; set;} + public string Street { get; set;} + + [NotColumn, NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore] + public string FullAddress => this.ComposeFullAddress() ?? string.Empty; + + [Association(ThisKey = nameof(PartnerId), OtherKey = nameof(Partner.Id), CanBeNull = true)] + public Partner? Partner { get; set; } + + [SkipValuesOnUpdate] + public DateTime Created { get; set; } + public DateTime Modified { get; set; } +} \ No newline at end of file diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index 74958298..e608f89c 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -30,6 +30,9 @@ public static class FruitBankConstClient public const string PalletDbTableName = "fbPallet"; public const string FilesDbTableName = "fbFiles"; public const string PartnerDbTableName = "fbPartner"; + public const string PartnerDepotDbTableName = "fbPartnerDepot"; + + public const string EkaerHistoryDbTableName = "fbEkaerHistory"; public const string OrderItemPalletDbTableName = "fbOrderItemPallet"; diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index 298ca2ef..f49ba450 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -112,6 +112,17 @@ public class SignalRTags : AcSignalRTags public const int GetStockTakingItemsByStockTakingId = 178; public const int AddOrUpdateMeasuredStockTakingItemPallet = 179; + public const int GetPartnerDepots = 180; + public const int GetPartnerDepotById = 181; + public const int GetPartnerDepotsByPartnerId = 182; + public const int AddPartnerDepot = 183; + public const int UpdatePartnerDepot = 184; + + public const int GetEkaerHistories = 185; + public const int GetEkaerHistoryById = 186; + //public const int GetEkaerHistoriesByForeignKey = 187; + public const int AddEkaerHistory = 188; + public const int UpdateEkaerHistory = 189; public const int AuthenticateUser = 195; public const int RefreshToken = 200; diff --git a/SqlSchemaCompare_Dev_to_Prod.scmp b/SqlSchemaCompare_Dev_to_Prod.scmp index f22fcf7e..29d855b1 100644 --- a/SqlSchemaCompare_Dev_to_Prod.scmp +++ b/SqlSchemaCompare_Dev_to_Prod.scmp @@ -3,12 +3,12 @@ 10 - Data Source=195.26.231.218;Initial Catalog=FruitBank_DEV;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True + Data Source=100.73.220.50;Initial Catalog=FruitBank_DEV;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Trust Server Certificate=True - Data Source=195.26.231.218;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True + Data Source=100.73.220.50;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Trust Server Certificate=True @@ -357,7 +357,7 @@ TargetConnectionString - Data Source=195.26.231.218;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True;Application Name="Microsoft SQL Server Data Tools, Schema Compare" + Data Source=100.73.220.50;Initial Catalog=FruitBank_PROD;Integrated Security=False;Persist Security Info=False;User ID=sa;Pooling=False;Multiple Active Result Sets=False;Trust Server Certificate=True;Application Name="Microsoft SQL Server Data Tools, Schema Compare" TreatVerificationErrorsAsWarnings @@ -1002,17 +1002,13 @@ dbo PK_AvalaraItemClassification - - dbo - FacebookPixelConfiguration - dbo PK_FacebookPixelConfiguration dbo - GoogleAuthenticatorRecord + FacebookPixelConfiguration dbo @@ -1020,12 +1016,16 @@ dbo - TaxTransactionLog + GoogleAuthenticatorRecord dbo PK_TaxTransactionLog + + dbo + TaxTransactionLog + dbo vOrder From 3b22b07d02fb76f885644f786b2120f0f5cc0bee Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 7 Jun 2026 07:20:07 +0200 Subject: [PATCH 02/17] PartnerDepot API, SignalRTags fixes, and test for duplicates - Added PartnerDepot CRUD API to IFruitBankDataControllerCommon and FruitBankSignalRClient. - Fixed Partner entity association to ShippingDocument.PartnerId. - Changed EkaerHistory to sealed, added ForeignKey, IsOutgoing, Created. - Exposed GetEkaerHistoriesByForeignKey in SignalRTags. - Fixed SignalRTags usage in ShippingDocument queries. - Implemented ProductDtoTableItem deserialization workaround. - Added unit test to detect duplicate SignalRTags constant values. --- FruitBank.Common/Entities/EkaerHistory.cs | 2 +- FruitBank.Common/Entities/Partner.cs | 2 +- .../IFruitBankDataControllerCommon.cs | 8 ++++ FruitBank.Common/SignalRs/SignalRTags.cs | 2 +- .../SignalRTagsTests.cs | 38 +++++++++++++++++++ .../Databases/DatabaseClient.cs | 2 + .../SignalRs/FruitBankSignalRClient.cs | 35 +++++++++++++---- 7 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 FruitBankHybrid.Shared.Tests/SignalRTagsTests.cs diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs index 70fba14a..ed3920fd 100644 --- a/FruitBank.Common/Entities/EkaerHistory.cs +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -11,7 +11,7 @@ namespace FruitBank.Common.Entities; [Table(Name = FruitBankConstClient.EkaerHistoryDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.EkaerHistoryDbTableName)] -public abstract class EkaerHistory: MgEntityBase, ITimeStampInfo +public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo { public int ForeignKey { get; set; } public bool IsOutgoing { get; set; } diff --git a/FruitBank.Common/Entities/Partner.cs b/FruitBank.Common/Entities/Partner.cs index 0acd2457..db482074 100644 --- a/FruitBank.Common/Entities/Partner.cs +++ b/FruitBank.Common/Entities/Partner.cs @@ -11,6 +11,6 @@ namespace FruitBank.Common.Entities; [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)] public sealed class Partner : PartnerBase, IPartner { - [Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.ShippingId), CanBeNull = true)] + [Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.PartnerId), CanBeNull = true)] public List? ShippingDocuments { get; set; } } \ No newline at end of file diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 39a8ae26..ac6d0730 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -20,6 +20,14 @@ public interface IFruitBankDataControllerCommon public Task UpdatePartner(Partner partner); #endregion Partner + #region PartnerDepot + public Task?> GetPartnerDepots(); + public Task GetPartnerDepotById(int id); + public Task?> GetPartnerDepotsByPartnerId(int partnerId); + public Task AddPartnerDepot(PartnerDepot partnerDepot); + public Task UpdatePartnerDepot(PartnerDepot partnerDepot); + #endregion PartnerDepot + #region CargoPartner public Task?> GetCargoPartners(); public Task GetCargoPartnerById(int id); diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index f49ba450..3c23bee4 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -120,7 +120,7 @@ public class SignalRTags : AcSignalRTags public const int GetEkaerHistories = 185; public const int GetEkaerHistoryById = 186; - //public const int GetEkaerHistoriesByForeignKey = 187; + public const int GetEkaerHistoriesByForeignKey = 187; public const int AddEkaerHistory = 188; public const int UpdateEkaerHistory = 189; diff --git a/FruitBankHybrid.Shared.Tests/SignalRTagsTests.cs b/FruitBankHybrid.Shared.Tests/SignalRTagsTests.cs new file mode 100644 index 00000000..42cfef12 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/SignalRTagsTests.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using FruitBank.Common.SignalRs; + +namespace FruitBankHybrid.Shared.Tests +{ + [TestClass] + public sealed class SignalRTagsTests + { + /// + /// A értékei a drót-protokoll azonosítói: két azonos értékű + /// konstans (jellemzően copy-paste után) némán másik endpointra irányítaná a hívást. + /// A kézi számkiosztás szándékos (stabil szerződés), ez a teszt csak alá feszít egy hálót: + /// kibukik a duplikátumon, és megnevezi a vétkes konstansokat. + /// + /// + /// Csak a -ben DEKLARÁLT konstansokat vizsgálja (DeclaredOnly) — + /// ide ír a fejlesztő kézzel. A framework-ős AcSignalRTags-szal való átfedés + /// ellenőrzéséhez BindingFlags.FlattenHierarchy kellene. + /// + [TestMethod] + public void SignalRTags_HasNoDuplicateValues() + { + var tags = typeof(SignalRTags) + .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(f => f.IsLiteral && f.FieldType == typeof(int)) + .Select(f => (f.Name, Value: (int)f.GetRawConstantValue()!)) + .ToList(); + + var duplicates = tags + .GroupBy(t => t.Value) + .Where(g => g.Count() > 1) + .Select(g => $" {g.Key} = {string.Join(", ", g.Select(t => t.Name))}") + .ToList(); + + Assert.AreEqual(0, duplicates.Count, $"Több SignalRTag ugyanazt az értéket kapta:{Environment.NewLine}{string.Join(Environment.NewLine, duplicates)}"); + } + } +} diff --git a/FruitBankHybrid.Shared/Databases/DatabaseClient.cs b/FruitBankHybrid.Shared/Databases/DatabaseClient.cs index 7c7e3531..7968b9e8 100644 --- a/FruitBankHybrid.Shared/Databases/DatabaseClient.cs +++ b/FruitBankHybrid.Shared/Databases/DatabaseClient.cs @@ -70,6 +70,8 @@ public class ProductDtoTable(FruitBankSignalRClient fruitBankSignalRClient) : Ac //Clear(); this.Replace(items); + //this.Replace(items.Cast()); + //foreach (var productDto in items) //{ // this.UpdateCollection(productDto, false); diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index bf590cd1..00a6854b 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -66,6 +66,14 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task UpdatePartner(Partner partner) => PostDataAsync(SignalRTags.UpdatePartner, partner); #endregion Partner + #region PartnerDepot + public Task?> GetPartnerDepots() => GetAllAsync>(SignalRTags.GetPartnerDepots); + public Task GetPartnerDepotById(int id) => GetByIdAsync(SignalRTags.GetPartnerDepotById, id); + public Task?> GetPartnerDepotsByPartnerId(int partnerId) => GetAllAsync>(SignalRTags.GetPartnerDepotsByPartnerId, [partnerId]); + public Task AddPartnerDepot(PartnerDepot partnerDepot) => PostDataAsync(SignalRTags.AddPartnerDepot, partnerDepot); + public Task UpdatePartnerDepot(PartnerDepot partnerDepot) => PostDataAsync(SignalRTags.UpdatePartnerDepot, partnerDepot); + #endregion PartnerDepot + #region CargoPartner public Task?> GetCargoPartners() => GetAllAsync>(SignalRTags.GetCargoPartners); public Task GetCargoPartnerById(int id) => GetByIdAsync(SignalRTags.GetCargoPartnerById, id); @@ -163,10 +171,10 @@ namespace FruitBankHybrid.Shared.Services.SignalRs => GetAllAsync>(SignalRTags.GetShippingDocumentsByShippingId, [shippingId]); public Task?> GetShippingDocumentsByProductId(int productId) - => GetAllAsync>(SignalRTags.GetShippingDocumentsByShippingId, [productId]); + => GetAllAsync>(SignalRTags.GetShippingDocumentsByProductId, [productId]); public Task?> GetShippingDocumentsByPartnerId(int partnerId) - => GetAllAsync>(SignalRTags.GetShippingDocumentsByShippingId, [partnerId]); + => GetAllAsync>(SignalRTags.GetShippingDocumentsByPartnerId, [partnerId]); public Task AddShippingDocument(ShippingDocument shippingDocument) => PostDataAsync(SignalRTags.AddShippingDocument, shippingDocument); @@ -185,18 +193,29 @@ namespace FruitBankHybrid.Shared.Services.SignalRs #endregion Customer #region Product + //TODO: itt ProductDtoTableItem kéne, csak az AycodeHubProtocol ProductDto-ra deserialize-ol! - J. + //public Task?> GetProductDtoTableItems() => GetAllAsync>(SignalRTags.GetProductDtos); + // IDEIGLENES gyors fix: a GetProductDtos tagon a szerver List-t küld, a binár + // protokoll viszont a wire-típusba (ProductDto) deszerializál — így a közvetlen + // List kérés üres lett (a ProductDtoTableItem a ProductDto kliens-oldali + // leszármazottja, amit a szerver nem ismer). A működő GetProductDtos()-t hívjuk, és bináris + // round-trippel a kért leszármazottra alakítjuk (azonos mezők). VÉGLEGES megoldás: requestId → + // kért-típus a protokollban (AQN-leváltás) — lásd AyCode.Services/docs/SIGNALR_BINARY_PROTOCOL. + public async Task?> GetProductDtoTableItems() + { + var dtos = await GetProductDtos(); + if (dtos == null) return null; - public Task?> GetProductDtoTableItems() - => GetAllAsync>(SignalRTags.GetProductDtos); + var bytes = SignalRSerializationHelper.SerializeToBinary(dtos); + return SignalRSerializationHelper.DeserializeFromBinary>(bytes); + } - public Task?> GetProductDtos() - => GetAllAsync>(SignalRTags.GetProductDtos); + public Task?> GetProductDtos() => GetAllAsync>(SignalRTags.GetProductDtos); //public Task?> GetAllMeasuringProductDtos() // => GetAllAsync>(SignalRTags.GetAllMeasuringProductDtos); - public Task GetProductDtoById(int productId) - => GetByIdAsync(SignalRTags.GetMeasuringProductDtoById, productId); + public Task GetProductDtoById(int productId) => GetByIdAsync(SignalRTags.GetMeasuringProductDtoById, productId); #endregion Product From 9a0d5698f02f5a057ac805ecec01d60f86f00500 Mon Sep 17 00:00:00 2001 From: Loretta Date: Mon, 8 Jun 2026 16:53:27 +0200 Subject: [PATCH 03/17] Refactor: remove Server DLLs, unify SignalR references - Removed all AyCode.Services.Server and Server DLL references from projects and code; now use only AyCode.Services and AyCode.Services.SignalRs. - Updated all documentation links to reference AyCode.Services/docs/SIGNALR_DATASOURCE/README.md. - Enabled AOT compilation in FruitBankHybrid.Web.Client. - Added InternalsVisibleTo for AyCode.Services.Server.Tests to support internal testing. - Introduced AcBinaryProtocolThreadPoolTests for thread-pool regression and contract testing. - Added P2 feature TODO for per-message protocol mode selection in AcBinaryHubProtocol. - Cleaned up using directives and removed obsolete Server-specific references. - Minor formatting and whitespace improvements. --- .../FruitBankHybrid.Shared.csproj | 3 --- .../Pages/OrdersAdmin.razor.cs | 1 - .../SignalRs/FruitBankSignalRClient.cs | 1 - .../Services/SignalRs/SignalRDataSource.cs | 1 - .../FruitBankHybrid.Web.Client.csproj | 21 +++---------------- 5 files changed, 3 insertions(+), 24 deletions(-) diff --git a/FruitBankHybrid.Shared/FruitBankHybrid.Shared.csproj b/FruitBankHybrid.Shared/FruitBankHybrid.Shared.csproj index afd1b00e..2e1e8fe4 100644 --- a/FruitBankHybrid.Shared/FruitBankHybrid.Shared.csproj +++ b/FruitBankHybrid.Shared/FruitBankHybrid.Shared.csproj @@ -43,9 +43,6 @@ ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll diff --git a/FruitBankHybrid.Shared/Pages/OrdersAdmin.razor.cs b/FruitBankHybrid.Shared/Pages/OrdersAdmin.razor.cs index ecabc3c2..5c03db52 100644 --- a/FruitBankHybrid.Shared/Pages/OrdersAdmin.razor.cs +++ b/FruitBankHybrid.Shared/Pages/OrdersAdmin.razor.cs @@ -1,6 +1,5 @@ using AyCode.Core.Interfaces; using AyCode.Core.Loggers; -using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DevExpress.Blazor; diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 00a6854b..dd696f71 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -1,7 +1,6 @@ using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Loggers; -using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using DevExpress.Utils.About; using FruitBank.Common; diff --git a/FruitBankHybrid.Shared/Services/SignalRs/SignalRDataSource.cs b/FruitBankHybrid.Shared/Services/SignalRs/SignalRDataSource.cs index 211341b0..29be99db 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/SignalRDataSource.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/SignalRDataSource.cs @@ -1,7 +1,6 @@ using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using AyCode.Core.Loggers; -using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using System.Collections.ObjectModel; using System.Diagnostics; diff --git a/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj b/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj index fc083e74..dcc2269b 100644 --- a/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj +++ b/FruitBankHybrid.Web.Client/FruitBankHybrid.Web.Client.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -7,7 +7,7 @@ true Default - false + true true true @@ -51,33 +51,18 @@ ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll - - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll - ..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll @@ -87,7 +72,7 @@ - ..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll + ..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll From b8f52e0c843c3bc17f6d660ab145093ab611a9cc Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 9 Jun 2026 14:51:23 +0200 Subject: [PATCH 04/17] =?UTF-8?q?Add=20PartnerDepot=20grid,=20EK=C3=81ER?= =?UTF-8?q?=20menu,=20and=20test=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added PartnerDepots navigation to Partner entity/interface - Introduced GridPartnerDepot component and detail tab in partner grid - Added EKÁER menu entry with icon and supporting styles - Moved partner/cargo tests to FruitBankPartnerTests; added PartnerDepot tests - Improved test assertions for clarity - Enabled AOT compilation by default in project file - Updated SignalR binary protocol doc with send-path analysis --- FruitBank.Common/Entities/Partner.cs | 3 + FruitBank.Common/Interfaces/IPartner.cs | 1 + .../FruitBankClientTests.cs | 153 +----------- .../FruitBankPartnerTests.cs | 225 ++++++++++++++++++ .../Grids/Partners/GridPartner.razor | 10 +- .../Grids/Partners/GridPartnerDepot.razor | 124 ++++++++++ .../Grids/Partners/GridPartnerDepotBase.cs | 85 +++++++ FruitBankHybrid.Shared/Layout/NavMenu.razor | 6 + .../Layout/NavMenu.razor.css | 4 + FruitBankHybrid.Shared/wwwroot/app.css | 1 + .../wwwroot/images/ekaer-fluent.svg | 4 + FruitBankHybrid/FruitBankHybrid.csproj | 6 +- 12 files changed, 470 insertions(+), 152 deletions(-) create mode 100644 FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs create mode 100644 FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor create mode 100644 FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepotBase.cs create mode 100644 FruitBankHybrid.Shared/wwwroot/images/ekaer-fluent.svg diff --git a/FruitBank.Common/Entities/Partner.cs b/FruitBank.Common/Entities/Partner.cs index db482074..45d4205e 100644 --- a/FruitBank.Common/Entities/Partner.cs +++ b/FruitBank.Common/Entities/Partner.cs @@ -11,6 +11,9 @@ namespace FruitBank.Common.Entities; [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)] public sealed class Partner : PartnerBase, IPartner { + [Association(ThisKey = nameof(Id), OtherKey = nameof(PartnerDepot.PartnerId), CanBeNull = true)] + public List? PartnerDepots { get; set; } + [Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.PartnerId), CanBeNull = true)] public List? ShippingDocuments { get; set; } } \ No newline at end of file diff --git a/FruitBank.Common/Interfaces/IPartner.cs b/FruitBank.Common/Interfaces/IPartner.cs index 5518e234..953fc33a 100644 --- a/FruitBank.Common/Interfaces/IPartner.cs +++ b/FruitBank.Common/Interfaces/IPartner.cs @@ -13,6 +13,7 @@ public interface ICargoPartner : IPartnerBase public interface IPartner : IPartnerBase { + List? PartnerDepots { get; set; } List? ShippingDocuments { get; set; } } diff --git a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs index ed23b765..1d2fe8fe 100644 --- a/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs +++ b/FruitBankHybrid.Shared.Tests/FruitBankClientTests.cs @@ -34,149 +34,6 @@ namespace FruitBankHybrid.Shared.Tests _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); } - #region Partner - - [TestMethod] - public async Task GetPartnersTest() - { - var partners = await _signalRClient.GetPartners(); - - Assert.IsNotNull(partners); - Assert.IsNotEmpty(partners); - } - - //[TestMethod] - //[DataRow(1)] - public async Task GetPartnerByIdTest(int partnerId) - { - var partner = await _signalRClient.GetPartnerById(partnerId); - - Assert.IsNotNull(partner); - Assert.AreEqual(partnerId, partner.Id); - - return partner; - } - - [TestMethod] - [DataRow(2)] - public async Task UpdatePartnerTest(int partnerId) - { - var partner = await GetPartnerByIdTest(partnerId); - - var newName = GetFixtureName(partner.Name); - - partner.Name = newName; - partner = await _signalRClient.UpdatePartner(partner); - - Assert.IsNotNull(partner); - Assert.IsTrue(partner.Name == newName); - - partner.Name = GetOriginalName(partner.Name); - partner = await _signalRClient.UpdatePartner(partner); - - Assert.IsNotNull(partner); - Assert.AreEqual(partnerId, partner.Id); - } - #endregion Partner - - #region CargoPartner - [TestMethod] - public async Task GetCargoPartnersTest() - { - var partners = await _signalRClient.GetCargoPartners(); - - Assert.IsNotNull(partners); - Assert.IsNotEmpty(partners); - } - //[TestMethod] - //[DataRow(1)] - public async Task GetCargoPartnerByIdTest(int cargoPartnerId) - { - var cargoPartner = await _signalRClient.GetCargoPartnerById(cargoPartnerId); - - Assert.IsNotNull(cargoPartner); - Assert.AreEqual(cargoPartnerId, cargoPartner.Id); - - return cargoPartner; - } - - [TestMethod] - [DataRow(1)] - public async Task UpdateCargoPartnerTest(int cargoPartnerId) - { - var cargoPartner = await GetCargoPartnerByIdTest(cargoPartnerId); - - var newName = GetFixtureName(cargoPartner.Name); - - cargoPartner.Name = newName; - cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner); - - Assert.IsNotNull(cargoPartner); - Assert.IsTrue(cargoPartner.Name == newName); - - cargoPartner.Name = GetOriginalName(cargoPartner.Name); - cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner); - - Assert.IsNotNull(cargoPartner); - Assert.AreEqual(cargoPartnerId, cargoPartner.Id); - } - #endregion CargoPartner - - #region CargoTruck - [TestMethod] - public async Task GetCargoTrucksTest() - { - var cargoTrucks = await _signalRClient.GetCargoTrucks(); - - Assert.IsNotNull(cargoTrucks); - Assert.IsNotEmpty(cargoTrucks); - } - - //[TestMethod] - //[DataRow(1)] - public async Task GetCargoTruckByIdTest(int cargoTruckId) - { - var cargoTruck = await _signalRClient.GetCargoTruckById(cargoTruckId); - - Assert.IsNotNull(cargoTruck); - Assert.AreEqual(cargoTruckId, cargoTruck.Id); - - return cargoTruck; - } - - [TestMethod] - [DataRow(1)] - public async Task GetCargoTrucksByCargoPartnerIdTest(int cargoPartnerId) - { - var cargoTrucks = await _signalRClient.GetCargoTrucksByCargoPartnerId(cargoPartnerId); - - Assert.IsNotNull(cargoTrucks); - Assert.IsNotEmpty(cargoTrucks); - } - - - [TestMethod] - [DataRow(1)] - public async Task UpdateCargoTruckTest(int cargoTruckId) - { - var cargoTruck = await GetCargoTruckByIdTest(cargoTruckId); - - var newLicencePlate = GetFixtureName(cargoTruck.LicencePlate); - - cargoTruck.LicencePlate = newLicencePlate; - cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck); - - Assert.IsNotNull(cargoTruck); - Assert.IsTrue(cargoTruck.LicencePlate == newLicencePlate); - - cargoTruck.LicencePlate = GetOriginalName(cargoTruck.LicencePlate); - cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck); - - Assert.IsNotNull(cargoTruck); - Assert.AreEqual(cargoTruckId, cargoTruck.Id); - } - #endregion CargoTruck - #region Shipping [TestMethod] public async Task GetShippingsTest() @@ -299,7 +156,7 @@ namespace FruitBankHybrid.Shared.Tests else { Assert.IsNotNull(shippingItem.ShippingDocument); - Assert.IsTrue(shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count == 0); + Assert.AreEqual(0, shippingItem.ShippingDocument.ShippingDocumentToFiles?.Count); } } @@ -313,11 +170,11 @@ namespace FruitBankHybrid.Shared.Tests if (shippingItem.IsMeasurable) Assert.IsNotNull(shippingItem.Pallet, $"shippingItem.Pallet == null; shippingItem.PalletId: {shippingItem.PalletId}"); Assert.IsNotNull(shippingItem.ProductDto, $"shippingItem.Product == null; shippingItem.ProductId: {shippingItem.ProductId}"); - Assert.IsTrue(shippingItem.Id == shippingItemId); + Assert.AreEqual(shippingItemId, shippingItem.Id); - Assert.IsTrue(shippingItem.QuantityOnDocument > 0, "QuantityOnDocument == 0"); - Assert.IsTrue(shippingItem.NetWeightOnDocument > 0, "NetWeightOnDocument == 0"); - Assert.IsTrue(shippingItem.GrossWeightOnDocument > 0, "GrossWeightOnDocument == 0"); + Assert.IsGreaterThan(0, shippingItem.QuantityOnDocument, "QuantityOnDocument == 0"); + Assert.IsGreaterThan(0, shippingItem.NetWeightOnDocument, "NetWeightOnDocument == 0"); + Assert.IsGreaterThan(0, shippingItem.GrossWeightOnDocument, "GrossWeightOnDocument == 0"); return shippingItem; } diff --git a/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs new file mode 100644 index 00000000..a3cc4214 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/FruitBankPartnerTests.cs @@ -0,0 +1,225 @@ +using FruitBank.Common; +using FruitBank.Common.Entities; +using FruitBankHybrid.Shared.Services.SignalRs; + +// ReSharper disable CompareOfFloatsByEqualityOperator + +namespace FruitBankHybrid.Shared.Tests +{ + [TestClass] + public sealed class FruitBankPartnerTests + { + private const int CustomerIdAasdDsserverCom = 6;//aasd@dsserver.com + private const string Fixture = "_test.temp"; + + private FruitBankSignalRClient _signalRClient = null!; + + private static string GetFixtureName(string name) => $"{GetOriginalName(name)}{Fixture}"; + private static string GetOriginalName(string name) => name.Replace(Fixture, string.Empty); + + [TestInitialize] + public void TestInit() + { + if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests)); + } + + #region Partner + + [TestMethod] + public async Task GetPartnersTest() + { + var partners = await _signalRClient.GetPartners(); + + Assert.IsNotNull(partners); + Assert.IsNotEmpty(partners); + } + + //[TestMethod] + //[DataRow(1)] + public async Task GetPartnerByIdTest(int partnerId) + { + var partner = await _signalRClient.GetPartnerById(partnerId); + + Assert.IsNotNull(partner); + Assert.AreEqual(partnerId, partner.Id); + + return partner; + } + + [TestMethod] + [DataRow(2)] + public async Task UpdatePartnerTest(int partnerId) + { + var partner = await GetPartnerByIdTest(partnerId); + + var newName = GetFixtureName(partner.Name); + + partner.Name = newName; + partner = await _signalRClient.UpdatePartner(partner); + + Assert.IsNotNull(partner); + Assert.AreEqual(newName, partner.Name); + + partner.Name = GetOriginalName(partner.Name); + partner = await _signalRClient.UpdatePartner(partner); + + Assert.IsNotNull(partner); + Assert.AreEqual(partnerId, partner.Id); + } + #endregion Partner + + #region CargoPartner + [TestMethod] + public async Task GetCargoPartnersTest() + { + var partners = await _signalRClient.GetCargoPartners(); + + Assert.IsNotNull(partners); + Assert.IsNotEmpty(partners); + } + //[TestMethod] + //[DataRow(1)] + public async Task GetCargoPartnerByIdTest(int cargoPartnerId) + { + var cargoPartner = await _signalRClient.GetCargoPartnerById(cargoPartnerId); + + Assert.IsNotNull(cargoPartner); + Assert.AreEqual(cargoPartnerId, cargoPartner.Id); + + return cargoPartner; + } + + [TestMethod] + [DataRow(1)] + public async Task UpdateCargoPartnerTest(int cargoPartnerId) + { + var cargoPartner = await GetCargoPartnerByIdTest(cargoPartnerId); + + var newName = GetFixtureName(cargoPartner.Name); + + cargoPartner.Name = newName; + cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner); + + Assert.IsNotNull(cargoPartner); + Assert.AreEqual(newName, cargoPartner.Name); + + cargoPartner.Name = GetOriginalName(cargoPartner.Name); + cargoPartner = await _signalRClient.UpdateCargoPartner(cargoPartner); + + Assert.IsNotNull(cargoPartner); + Assert.AreEqual(cargoPartnerId, cargoPartner.Id); + } + #endregion CargoPartner + + #region CargoTruck + [TestMethod] + public async Task GetCargoTrucksTest() + { + var cargoTrucks = await _signalRClient.GetCargoTrucks(); + + Assert.IsNotNull(cargoTrucks); + Assert.IsNotEmpty(cargoTrucks); + } + + //[TestMethod] + //[DataRow(1)] + public async Task GetCargoTruckByIdTest(int cargoTruckId) + { + var cargoTruck = await _signalRClient.GetCargoTruckById(cargoTruckId); + + Assert.IsNotNull(cargoTruck); + Assert.AreEqual(cargoTruckId, cargoTruck.Id); + + return cargoTruck; + } + + [TestMethod] + [DataRow(1)] + public async Task GetCargoTrucksByCargoPartnerIdTest(int cargoPartnerId) + { + var cargoTrucks = await _signalRClient.GetCargoTrucksByCargoPartnerId(cargoPartnerId); + + Assert.IsNotNull(cargoTrucks); + Assert.IsNotEmpty(cargoTrucks); + } + + + [TestMethod] + [DataRow(1)] + public async Task UpdateCargoTruckTest(int cargoTruckId) + { + var cargoTruck = await GetCargoTruckByIdTest(cargoTruckId); + + var newLicencePlate = GetFixtureName(cargoTruck.LicencePlate); + + cargoTruck.LicencePlate = newLicencePlate; + cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck); + + Assert.IsNotNull(cargoTruck); + Assert.AreEqual(newLicencePlate, cargoTruck.LicencePlate); + + cargoTruck.LicencePlate = GetOriginalName(cargoTruck.LicencePlate); + cargoTruck = await _signalRClient.UpdateCargoTruck(cargoTruck); + + Assert.IsNotNull(cargoTruck); + Assert.AreEqual(cargoTruckId, cargoTruck.Id); + } + #endregion CargoTruck + + #region PartnerDepot + [TestMethod] + public async Task GetPartnerDepotsTest() + { + var partnerDepots = await _signalRClient.GetPartnerDepots(); + + Assert.IsNotNull(partnerDepots); + Assert.IsNotEmpty(partnerDepots); + } + + //[TestMethod] + //[DataRow(1)] + public async Task GetPartnerDepotByIdTest(int partnerDepotId) + { + var partnerDepot = await _signalRClient.GetPartnerDepotById(partnerDepotId); + + Assert.IsNotNull(partnerDepot); + Assert.AreEqual(partnerDepotId, partnerDepot.Id); + + return partnerDepot; + } + + [TestMethod] + [DataRow(1)] + public async Task GetPartnerDepotsByPartnerIdTest(int partnerId) + { + var partnerDepots = await _signalRClient.GetPartnerDepotsByPartnerId(partnerId); + + Assert.IsNotNull(partnerDepots); + Assert.IsNotEmpty(partnerDepots); + } + + [TestMethod] + [DataRow(1)] + public async Task UpdatePartnerDepotTest(int partnerDepotId) + { + var partnerDepot = await GetPartnerDepotByIdTest(partnerDepotId); + + var newName = GetFixtureName(partnerDepot.Name); + + partnerDepot.Name = newName; + partnerDepot = await _signalRClient.UpdatePartnerDepot(partnerDepot); + + Assert.IsNotNull(partnerDepot); + Assert.AreEqual(newName, partnerDepot.Name); + + partnerDepot.Name = GetOriginalName(partnerDepot.Name); + partnerDepot = await _signalRClient.UpdatePartnerDepot(partnerDepot); + + Assert.IsNotNull(partnerDepot); + Assert.AreEqual(partnerDepotId, partnerDepot.Id); + } + #endregion PartnerDepot + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor index 0bc66eb8..61947300 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor @@ -29,7 +29,7 @@ - + @@ -46,6 +46,7 @@ @if (IsMasterGrid) { var partner = ((Partner)context.DataItem); + var partnerDepots = partner?.PartnerDepots ?? []; var shippingDocuments = partner?.ShippingDocuments ?? []; @@ -62,6 +63,13 @@ } + + + @{ + var observablePartnerDepots = new AcObservableCollection(partnerDepots); + + } + } diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor new file mode 100644 index 00000000..26a090f1 --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor @@ -0,0 +1,124 @@ +@using System.Collections.ObjectModel +@using AyCode.Blazor.Components.Components.Grids +@using AyCode.Core.Helpers +@using AyCode.Core.Interfaces +@using AyCode.Core.Loggers +@using AyCode.Utils.Extensions +@using FruitBank.Common.Dtos +@using FruitBank.Common.Entities +@using FruitBankHybrid.Shared.Components.Grids.Shippings +@using FruitBankHybrid.Shared.Databases +@using FruitBankHybrid.Shared.Services.Loggers +@using FruitBankHybrid.Shared.Services.SignalRs + +@inject IEnumerable LogWriters +@inject FruitBankSignalRClient FruitBankSignalRClient + + + + + + + + + + + + + + + + + + + @* + @if (IsMasterGrid) + { + var partnerDepot = ((PartnerDepot)context.DataItem); + + + + @{ + var observableShippingDocuments = new AcObservableCollection(shippingDocuments); + + } + + + + @{ + var observableShippingItems = new AcObservableCollection(shippingDocuments.SelectMany(sd => sd.ShippingItems ?? [])); + + } + + + } + + *@ + @if (IsMasterGrid) + { + + } + + + + + +@code { + //[Inject] public required ObjectLock ObjectLock { get; set; } + [Inject] public required DatabaseClient Database { get; set; } + + [Parameter] public AcObservableCollection? PartnerDepots { get; set; } + + const string ExportFileName = "ExportResult"; + string GridSearchText = ""; + bool EditItemsEnabled { get; set; } + int FocusedRowVisibleIndex { get; set; } + public GridPartnerDepotBase Grid { get; set; } + string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty; + + [Parameter] public IId? ParentDataItem { get; set; } + + public bool IsMasterGrid => ParentDataItem == null; + public bool ParentDataItemIsPartner => (ParentDataItem is Partner); + + private int _activeTabIndex; + 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/Partners/GridPartnerDepotBase.cs b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepotBase.cs new file mode 100644 index 00000000..c62b4e50 --- /dev/null +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepotBase.cs @@ -0,0 +1,85 @@ +using AyCode.Core.Interfaces; +using AyCode.Utils.Extensions; +using DevExpress.Blazor; +using FruitBank.Common.Entities; +using FruitBank.Common.Interfaces; +using FruitBank.Common.SignalRs; +using FruitBankHybrid.Shared.Pages; +using Microsoft.AspNetCore.Components; + +namespace FruitBankHybrid.Shared.Components.Grids.Partners; + +public class GridPartnerDepotBase: FruitBankGridBase, IGrid +{ + private bool _isFirstInitializeParameterCore; + private bool _isFirstInitializeParameters; + + public GridPartnerDepotBase() : base() + { + //GetAllMessageTag = SignalRTags.GetPartnerDepots; + AddMessageTag = SignalRTags.AddPartnerDepot; + UpdateMessageTag = SignalRTags.UpdatePartnerDepot; + + //RemoveMessageTag = SignalRTags.; + } + + protected override async Task OnInitializedAsync() + { + if (GetAllMessageTag > 0) return; + + if (IsMasterGrid) GetAllMessageTag = SignalRTags.GetPartnerDepots; + else + { + if (ContextIds == null || ContextIds.Length == 0) ContextIds = [ParentDataItem!.Id]; + + switch (ParentDataItem) + { + case IPartner: + GetAllMessageTag = SignalRTags.GetPartnerDepotsByPartnerId; + if (KeyFieldNameToParentId.IsNullOrWhiteSpace()) KeyFieldNameToParentId = nameof(PartnerDepot.PartnerId); + + break; + } + } + + await base.OnInitializedAsync(); + } + + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (!_isFirstInitializeParameters) + { + //if (!IsMasterGrid && (ContextIds == null || ContextIds.Length == 0)) + //{ + // ContextIds = [ParentDataItem!.Id]; + // GetAllMessageTag = SignalRTags.GetShippingItemsByDocumentId; + //} + + _isFirstInitializeParameters = false; + } + } + + protected override async Task SetParametersAsyncCore(ParameterView parameters) + { + await base.SetParametersAsyncCore(parameters); + + if (!_isFirstInitializeParameterCore) + { + //if (!IsMasterGrid && (ContextIds == null || ContextIds.Length == 0)) + //{ + // ContextIds = [ParentDataItem!.Id]; + // GetAllMessageTag = SignalRTags.GetShippingItemsByDocumentId; + //} + + //ShowFilterRow = true; + //ShowGroupPanel = true; + //AllowSort = false; + + //etc... + + _isFirstInitializeParameterCore = false; + } + } +} \ No newline at end of file diff --git a/FruitBankHybrid.Shared/Layout/NavMenu.razor b/FruitBankHybrid.Shared/Layout/NavMenu.razor index 03b90813..ca3286ad 100644 --- a/FruitBankHybrid.Shared/Layout/NavMenu.razor +++ b/FruitBankHybrid.Shared/Layout/NavMenu.razor @@ -63,6 +63,12 @@ Szállítmányok + + } } else diff --git a/FruitBankHybrid.Shared/Layout/NavMenu.razor.css b/FruitBankHybrid.Shared/Layout/NavMenu.razor.css index e1614b30..2987b88d 100644 --- a/FruitBankHybrid.Shared/Layout/NavMenu.razor.css +++ b/FruitBankHybrid.Shared/Layout/NavMenu.razor.css @@ -59,6 +59,10 @@ --icon-mask-image: var(--icon-login-mask-image); } +::deep .ekaer-icon { + --icon-mask-image: var(--icon-ekaer-mask-image); +} + .nav-item { font-size: 0.9rem; padding-bottom: 0.5rem; diff --git a/FruitBankHybrid.Shared/wwwroot/app.css b/FruitBankHybrid.Shared/wwwroot/app.css index 71e41aa0..52abce8f 100644 --- a/FruitBankHybrid.Shared/wwwroot/app.css +++ b/FruitBankHybrid.Shared/wwwroot/app.css @@ -64,6 +64,7 @@ h1:focus { --icon-orders-mask-image: url("images/orders-fluent.svg"); --icon-shippings-mask-image: url("images/shippings-fluent.svg"); --icon-login-mask-image: url("images/login-fluent.svg"); + --icon-ekaer-mask-image: url("images/ekaer-fluent.svg"); } .icon { diff --git a/FruitBankHybrid.Shared/wwwroot/images/ekaer-fluent.svg b/FruitBankHybrid.Shared/wwwroot/images/ekaer-fluent.svg new file mode 100644 index 00000000..7c6623f3 --- /dev/null +++ b/FruitBankHybrid.Shared/wwwroot/images/ekaer-fluent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/FruitBankHybrid/FruitBankHybrid.csproj b/FruitBankHybrid/FruitBankHybrid.csproj index a5bcd727..5d471d11 100644 --- a/FruitBankHybrid/FruitBankHybrid.csproj +++ b/FruitBankHybrid/FruitBankHybrid.csproj @@ -23,7 +23,7 @@ 1 $(ApplicationDisplayVersion) - false + true true true @@ -47,9 +47,9 @@ - + true From 735eabf008533b1d5544e328904b7c4353b70dd0 Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 9 Jun 2026 15:28:23 +0200 Subject: [PATCH 05/17] =?UTF-8?q?Add=20EK=C3=81ER=20admin=20Blazor=20page?= =?UTF-8?q?=20with=20tabs=20and=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced a new Blazor page for EKÁER administration, including a tabbed UI for pending and sent records (grids to be implemented). The code-behind handles user role checks, navigation, and logging initialization. Tab switching logic is stubbed for future development. --- FruitBankHybrid.Shared/Pages/Ekaer.razor | 25 +++++++++++++++ FruitBankHybrid.Shared/Pages/Ekaer.razor.cs | 34 +++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 FruitBankHybrid.Shared/Pages/Ekaer.razor create mode 100644 FruitBankHybrid.Shared/Pages/Ekaer.razor.cs diff --git a/FruitBankHybrid.Shared/Pages/Ekaer.razor b/FruitBankHybrid.Shared/Pages/Ekaer.razor new file mode 100644 index 00000000..d3443383 --- /dev/null +++ b/FruitBankHybrid.Shared/Pages/Ekaer.razor @@ -0,0 +1,25 @@ +@page "/Ekaer" +@using FruitBankHybrid.Shared.Components +@using FruitBankHybrid.Shared.Databases +

EKÁER - Adminisztrátor

+ + + +
+ + + + + + @* A beküldésre váró EKÁER-ek grid-je ide kerül — külön feladat (kliens-metódus, szűrés). *@ + + + @* 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 new file mode 100644 index 00000000..f3a306a4 --- /dev/null +++ b/FruitBankHybrid.Shared/Pages/Ekaer.razor.cs @@ -0,0 +1,34 @@ +using AyCode.Core.Loggers; +using FruitBank.Common.Models; +using FruitBankHybrid.Shared.Services.Loggers; +using Mango.Nop.Core.Loggers; +using Microsoft.AspNetCore.Components; + +namespace FruitBankHybrid.Shared.Pages; + +public partial class Ekaer : ComponentBase +{ + [Inject] public required IEnumerable LogWriters { get; set; } + [Inject] public required NavigationManager NavManager { get; set; } + [Inject] public required LoggedInModel LoggedInModel { get; set; } + + private ILogger _logger = null!; + public int ActiveTabIndex; + + protected override Task OnInitializedAsync() + { + if (!LoggedInModel.IsRevisor) NavManager.NavigateTo("/Login"); + + _logger = new LoggerClient(LogWriters.ToArray()); + _logger.Info("OnInitializedAsync"); + + return base.OnInitializedAsync(); + } + + protected Task OnActiveTabChanged(int activeTabIndex) + { + ActiveTabIndex = activeTabIndex; + // A fülek tartalma (EKÁER-grid) külön feladat — több előfeltétele van (kliens-metódus, szűrés). + return Task.CompletedTask; + } +} From 82f0f00196802a81f4d82b61891f6df12ef3d323 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 10 Jun 2026 16:24:54 +0200 Subject: [PATCH 06/17] =?UTF-8?q?Add=20EK=C3=81ER=20history=20grid,=20deta?= =?UTF-8?q?il=20view,=20and=20entity=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduced `EkaerHistory` entity with status enum and new properties for EKÁER declaration lifecycle tracking. - Added CRUD methods for `EkaerHistory` in controller interface and SignalR client. - Implemented `GridEkaerHistory` and `GridEkaerDetail` components for displaying EKÁER records and trade card details. - Enhanced `TradeCardItemType` with `IId` for grid compatibility. - Improved AcSignalRDataSource to skip server refresh for tagless/local data sources. - Updated settings to include .NET 9 runtime install commands. --- .claude/settings.local.json | 5 +- FruitBank.Common/Entities/EkaerHistory.cs | 35 +++++- .../IFruitBankDataControllerCommon.cs | 8 ++ .../Grids/Ekaers/GridEkaerDetail.razor | 86 +++++++++++++ .../Grids/Ekaers/GridEkaerDetailBase.cs | 19 +++ .../Grids/Ekaers/GridEkaerHistory.razor | 113 ++++++++++++++++++ .../Grids/Ekaers/GridEkaerHistoryBase.cs | 66 ++++++++++ FruitBankHybrid.Shared/Pages/Ekaer.razor | 6 +- FruitBankHybrid.Shared/Pages/Ekaer.razor.cs | 4 + .../SignalRs/FruitBankSignalRClient.cs | 8 ++ 10 files changed, 346 insertions(+), 4 deletions(-) create mode 100644 FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor create mode 100644 FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetailBase.cs create mode 100644 FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor create mode 100644 FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistoryBase.cs 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); From 76cb8adbe6eae56b2d23696698892fa52a75e222 Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 10 Jun 2026 18:09:33 +0200 Subject: [PATCH 07/17] =?UTF-8?q?Add=20EK=C3=81ER=20XML=20generation,=20va?= =?UTF-8?q?lidation,=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added GenerateEkaerXmlDocument to service and interface for EKÁER tradeCard XML generation and validation from ShippingDocument. - Extended IShippingToEkaerMapper to support document-level mapping. - Updated controller and SignalR client/interfaces with new methods and tags for EKÁER XML and EkaerHistory creation. - Formatted Created/Modified columns in Blazor grids. - Added tests for EkaerHistory creation, XML generation, and idempotency. - Improved null-safety and argument validation in mapping logic. --- .../Services/Ekaer/FruitBankEkaerService.cs | 28 +++- .../Services/Ekaer/IFruitBankEkaerService.cs | 10 ++ .../IFruitBankDataControllerCommon.cs | 2 + .../Services/Ekaer/IShippingToEkaerMapper.cs | 7 + .../Services/Ekaer/ShippingToEkaerMapper.cs | 16 ++- FruitBank.Common/SignalRs/SignalRTags.cs | 2 + .../FruitBankEkaerTests.cs | 123 ++++++++++++++++++ .../Grids/Cargos/GridCargoPartner.razor | 4 +- .../Grids/Cargos/GridCargoTruck.razor | 4 +- .../Grids/Ekaers/GridEkaerHistory.razor | 4 +- .../Grids/Partners/GridPartner.razor | 4 +- .../Grids/Partners/GridPartnerDepot.razor | 4 +- .../Grids/Shippings/GridShipping.razor | 4 +- .../SignalRs/FruitBankSignalRClient.cs | 2 + 14 files changed, 197 insertions(+), 17 deletions(-) create mode 100644 FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index 616cb277..c5384fb4 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -1,3 +1,4 @@ +using AyCode.Services.Nav; using AyCode.Services.Nav.Ekaer; using AyCode.Services.Nav.Ekaer.Models; using FruitBank.Common.Entities; @@ -15,12 +16,14 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService { private readonly IShippingToEkaerMapper _mapper; private readonly IEkaerSubmitService _submitService; + private readonly IEkaerTradeCardValidator _validator; private readonly EkaerCompanyInfo _company; - public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, EkaerCompanyInfo company) + public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerCompanyInfo company) { _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _submitService = submitService ?? throw new ArgumentNullException(nameof(submitService)); + _validator = validator ?? throw new ArgumentNullException(nameof(validator)); _company = company ?? throw new ArgumentNullException(nameof(company)); } @@ -32,4 +35,27 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService var operations = _mapper.MapShipping(shipping, _company, operation); return _submitService.SubmitAsync(operations, cancellationToken); } + + public EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null) + { + ArgumentNullException.ThrowIfNull(document); + + ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false }; + + var operation = new TradeCardOperationType + { + Index = 1, + Operation = OperationType.Create, + TradeCard = _mapper.MapDocument(document, _company), + }; + + var errors = _validator.Validate(operation); + + // Az XML validációs hibánál IS tárolódik — a detail-nézetben így látszik, mi hiányzik. + ekaerHistory.XmlDoc = NavXmlHelper.Serialize(operation.TradeCard); + ekaerHistory.Status = errors.Count == 0 ? EkaerStatus.Generated : EkaerStatus.ValidationError; + ekaerHistory.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage)); + + return ekaerHistory; + } } diff --git a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs index d9f13b9c..244bbf3a 100644 --- a/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/IFruitBankEkaerService.cs @@ -16,4 +16,14 @@ public interface IFruitBankEkaerService /// vagy a NAV-válasz — lásd . /// Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default); + + /// + /// Egy szállítólevélből legenerálja az EKÁER tradeCard XML-t és validálja — a (meglévő vagy új) + /// rekordot tölti: XmlDoc + Status + /// ( / ) + ErrorText. + /// NEM perzisztál és NEM hív NAV-ot — a mentés (upsert) a hívó SignalR endpoint dolga. + /// + /// A szállítólevél a betöltött gráffal (Partner, Items+ProductDto, Shipping→járművek). + /// A dokumentum meglévő rekordja (újrageneráláskor); null → új rekord készül. + EkaerHistory GenerateEkaerXmlDocument(ShippingDocument document, EkaerHistory? ekaerHistory = null); } diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 30c81db2..9e31b37c 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -34,6 +34,8 @@ public interface IFruitBankDataControllerCommon public Task?> GetEkaerHistoriesByForeignKey(int foreignKey); public Task AddEkaerHistory(EkaerHistory ekaerHistory); public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); + public Task GenerateEkaerXmlDocument(int shippingDocumentId); + public Task CreateEkaerHistory(int foreignKey, bool isOutgoing); #endregion EkaerHistory #region CargoPartner diff --git a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs index a3430daf..31b2b70b 100644 --- a/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/IShippingToEkaerMapper.cs @@ -22,4 +22,11 @@ public interface IShippingToEkaerMapper /// A bejelentő saját cégadatai (címzett bejövő relációban) + a lerakodási hely. /// A tradeCard művelet típusa. Alapértelmezés: . IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create); + + /// + /// EGY szállítólevelet () képez le egy tradeCard-dá — a dokumentum-szintű + /// EKÁER-granularitás egysége (1 dokumentum = 1 tradeCard = 1 TCN). A jármű/fuvarozó adatok a + /// document.Shipping-ből jönnek; ha az nincs betöltve, ezek üresen maradnak (a validátor jelzi). + /// + TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company); } diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs index c038c745..7b221ff3 100644 --- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -42,7 +42,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper return operations; } - private static TradeCardType BuildTradeCard(Shipping shipping, ShippingDocument document, EkaerCompanyInfo company) + public TradeCardType MapDocument(ShippingDocument document, EkaerCompanyInfo company) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(company); + + return BuildTradeCard(document.Shipping, document, company); + } + + private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company) { var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase @@ -64,7 +72,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper DestinationAddress = Truncate(company.FullAddress, 200), // Fuvarozó (Shipping.CargoPartner). Regisztrált EKAER-azonosító nincs, csak szöveges név. - CarrierText = shipping.CargoPartner?.Name, + CarrierText = shipping?.CargoPartner?.Name, // Lerakodás = saját telephely (a cégadatból); felrakodás = a beszállító telephelye. UnloadLocation = company.UnloadLocation, @@ -72,8 +80,8 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper }; // Vonó jármű + vontatmány: az EKÁER külön bejegyzésként kéri (vehicle / vehicle2). - if (shipping.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); - if (shipping.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); + if (shipping?.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); + if (shipping?.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item)); return tradeCard; diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index 3c23bee4..77e72728 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -123,6 +123,8 @@ public class SignalRTags : AcSignalRTags public const int GetEkaerHistoriesByForeignKey = 187; public const int AddEkaerHistory = 188; public const int UpdateEkaerHistory = 189; + public const int GenerateEkaerXmlDocument = 190; + public const int CreateEkaerHistory = 191; public const int AuthenticateUser = 195; public const int RefreshToken = 200; diff --git a/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs new file mode 100644 index 00000000..12bbe5e9 --- /dev/null +++ b/FruitBankHybrid.Shared.Tests/FruitBankEkaerTests.cs @@ -0,0 +1,123 @@ +using AyCode.Services.Nav; +using AyCode.Services.Nav.Ekaer.Models; +using FruitBank.Common; +using FruitBank.Common.Entities; +using FruitBankHybrid.Shared.Services.SignalRs; + +namespace FruitBankHybrid.Shared.Tests +{ + [TestClass] + public sealed class FruitBankEkaerTests + { + private FruitBankSignalRClient _signalRClient = null!; + + [TestInitialize] + public void TestInit() + { + if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!"); + + _signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankEkaerTests)); + } + + #region EkaerHistory / Create + + /// + /// Backfill: minden meglévő szállítólevélre létrehozza az EkaerHistory rekordot (Pending, XML nélkül). + /// A generálás a grid Generate gombjával, kézzel történik. Idempotens: újrafuttatva nem duplikál. + /// + [TestMethod] + public async Task CreateEkaerHistoryForAllShippingDocumentsTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + foreach (var shippingDocument in shippingDocuments) + { + var ekaerHistory = await _signalRClient.CreateEkaerHistory(shippingDocument.Id, false); + + Console.WriteLine($"doc#{shippingDocument.Id}: EkaerHistory Id: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Id.ToString())}; Status: {ekaerHistory?.Status}"); + + Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.IsGreaterThan(0, ekaerHistory.Id, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); + Assert.IsFalse(ekaerHistory.IsOutgoing); + } + + // Idempotencia: a második hívás a meglévőt adja vissza, nem duplikál. + var firstDocumentId = shippingDocuments[0].Id; + var again = await _signalRClient.CreateEkaerHistory(firstDocumentId, false); + var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(firstDocumentId); + + Assert.IsNotNull(again); + Assert.IsNotNull(histories); + Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {firstDocumentId}"); + } + + #endregion EkaerHistory / Create + + #region EkaerHistory / Generate + + /// + /// Backfill + teljes Generate-út teszt: minden meglévő szállítólevélre legenerálja az EKÁER XML-t + /// (rekord upsert a szerveren), így a grid valós adatot kap és a Generate gomb útvonala tesztelt. + /// + //[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog. + public async Task GenerateEkaerXmlDocumentForAllShippingDocumentsTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + foreach (var shippingDocument in shippingDocuments) + { + var ekaerHistory = await _signalRClient.GenerateEkaerXmlDocument(shippingDocument.Id); + + // A szerver által visszaadott állapot/hibalista logolása — az assertek ELŐTT, hogy hibánál is látsszon. + Console.WriteLine($"doc#{shippingDocument.Id}: Status: {(ekaerHistory == null ? "NULL VÁLASZ!" : ekaerHistory.Status.ToString())}"); + if (!string.IsNullOrWhiteSpace(ekaerHistory?.ErrorText)) Console.WriteLine($" ErrorText: {ekaerHistory.ErrorText}"); + + Assert.IsNotNull(ekaerHistory, $"shippingDocument.Id: {shippingDocument.Id}"); + Assert.AreEqual(shippingDocument.Id, ekaerHistory.ForeignKey); + Assert.IsFalse(ekaerHistory.IsOutgoing); + Assert.IsFalse(string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc), $"XmlDoc üres; shippingDocument.Id: {shippingDocument.Id}"); + Assert.IsTrue(ekaerHistory.Status is EkaerStatus.Generated or EkaerStatus.ValidationError, + $"Status: {ekaerHistory.Status}; shippingDocument.Id: {shippingDocument.Id}; ErrorText: {ekaerHistory.ErrorText}"); + + // A grid útvonala: az XmlDoc visszaolvasható tradeCard-dá. + var tradeCard = NavXmlHelper.Deserialize(ekaerHistory.XmlDoc!); + + Assert.IsNotNull(tradeCard); + Console.WriteLine($" items: {tradeCard.Items.Count}"); + } + } + + /// Idempotencia: az újragenerálás NEM duplikál — dokumentumonként egy bejövő rekord marad. + //[TestMethod] //Kikommentezve: a generálás a grid Generate gombjával, kézzel történik — a teszt később még kelleni fog. + public async Task GenerateEkaerXmlDocumentIsIdempotentTest() + { + var shippingDocuments = await _signalRClient.GetShippingDocuments(); + + Assert.IsNotNull(shippingDocuments); + Assert.IsNotEmpty(shippingDocuments); + + var shippingDocumentId = shippingDocuments[0].Id; + + var first = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId); + var second = await _signalRClient.GenerateEkaerXmlDocument(shippingDocumentId); + + Assert.IsNotNull(first); + Assert.IsNotNull(second); + Assert.AreEqual(first.Id, second.Id, "Az újragenerálás új rekordot hozott létre frissítés helyett!"); + + var histories = await _signalRClient.GetEkaerHistoriesByForeignKey(shippingDocumentId); + + Assert.IsNotNull(histories); + Assert.AreEqual(1, histories.Count(h => !h.IsOutgoing), $"Duplikált bejövő EkaerHistory; shippingDocumentId: {shippingDocumentId}"); + } + + #endregion EkaerHistory / Generate + } +} diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor index 07ea7f23..626bcf65 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor @@ -38,8 +38,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor index 3663e202..61144166 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoTruck.razor @@ -32,8 +32,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index ed881786..be85e3d2 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -29,8 +29,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor index 61947300..c4e9a301 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartner.razor @@ -38,8 +38,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor index 26a090f1..0862fd24 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Partners/GridPartnerDepot.razor @@ -35,8 +35,8 @@ - - + + @* diff --git a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor index c668cd3a..5a9a2378 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor @@ -86,8 +86,8 @@ - - + + diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 1f10aa12..8dbe9808 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -79,6 +79,8 @@ namespace FruitBankHybrid.Shared.Services.SignalRs 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); + public Task GenerateEkaerXmlDocument(int shippingDocumentId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, shippingDocumentId); + public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]); #endregion EkaerHistory #region CargoPartner From ea29ad18bdc7742e8259d08e47c9ac1e988744f5 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Jun 2026 08:52:10 +0200 Subject: [PATCH 08/17] =?UTF-8?q?EK=C3=81ER=20grid:=20status=20enum=20fix,?= =?UTF-8?q?=20batch=20create,=20UI/UX,=20DB=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed EkaerHistory status persistence (int+enum wrapper for linq2db) - Added batch EKÁER record creation (toolbar date picker, SignalR, UI) - Enhanced GridEkaerHistory: status, error, XML copy, manual edit - Improved column display format handling in MgGridInfoPanel - Updated SignalR binary protocol streaming strategy doc - Added DB guard PowerShell hook: blocks non-_DEV DB shell access - New styles for EKÁER toolbar date picker --- .claude/hooks/db-dev-guard.ps1 | 25 +++ .claude/settings.json | 17 ++ .claude/settings.local.json | 6 +- FruitBank.Common/Entities/EkaerHistory.cs | 18 ++- .../IFruitBankDataControllerCommon.cs | 1 + FruitBank.Common/SignalRs/SignalRTags.cs | 1 + .../Grids/Ekaers/GridEkaerDetail.razor | 9 ++ .../Grids/Ekaers/GridEkaerHistory.razor | 150 +++++++++++++++++- .../SignalRs/FruitBankSignalRClient.cs | 1 + FruitBankHybrid.Shared/wwwroot/app.css | 14 ++ 10 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 .claude/hooks/db-dev-guard.ps1 create mode 100644 .claude/settings.json diff --git a/.claude/hooks/db-dev-guard.ps1 b/.claude/hooks/db-dev-guard.ps1 new file mode 100644 index 00000000..4f212be8 --- /dev/null +++ b/.claude/hooks/db-dev-guard.ps1 @@ -0,0 +1,25 @@ +# Claude Code PreToolUse hook: DB-vedelem — csak _DEV nevu adatbazis erheto el shell-parancsbol. +# Bemenet: hook JSON a stdin-en; ha a parancs connection stringet tartalmaz (Initial Catalog= / Database=) +# es a DB-nev NEM tartalmazza a "_DEV"-et, a tool-hivast DENY-jal blokkolja. + +$payloadText = [Console]::In.ReadToEnd() +if ([string]::IsNullOrWhiteSpace($payloadText)) { exit 0 } + +try { $payload = $payloadText | ConvertFrom-Json } catch { exit 0 } + +$command = $payload.tool_input.command +if ([string]::IsNullOrWhiteSpace($command)) { exit 0 } + +$pattern = [regex]'(?i)(?:Initial\s+Catalog|Database)\s*=\s*([^;"''\s]+)' + +foreach ($match in $pattern.Matches($command)) { + $dbName = $match.Groups[1].Value + if ($dbName -notmatch '(?i)_DEV') { + $reason = "Blokkolt: csak _DEV adatbazis modosithato! (talalt adatbazis: $dbName)" + $result = @{ hookSpecificOutput = @{ hookEventName = 'PreToolUse'; permissionDecision = 'deny'; permissionDecisionReason = $reason } } + Write-Output ($result | ConvertTo-Json -Depth 5 -Compress) + exit 0 + } +} + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..96d98deb --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash|PowerShell", + "hooks": [ + { + "type": "command", + "command": "powershell -NoProfile -ExecutionPolicy Bypass -File \"H:/Applications/Mango/Source/FruitBankHybridApp/.claude/hooks/db-dev-guard.ps1\"", + "timeout": 30, + "statusMessage": "DB guard (_DEV) ellenőrzés..." + } + ] + } + ] + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d11af6ab..983a40cc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -57,7 +57,11 @@ "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\")" + "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\")", + "Skill(update-config)", + "Skill(update-config:*)", + "PowerShell($script = \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\.claude\\\\hooks\\\\db-dev-guard.ps1\"; \"--- A\\) PROD \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"sqlcmd -S 1.2.3.4 -Q \\\\\"x\\\\\" # Initial Catalog=FruitBank;User ID=sa\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- B\\) DEV \\(semmi vart\\):\"; '{\"tool_name\":\"PowerShell\",\"tool_input\":{\"command\":\"$cs = \\\\\"Data Source=x;Initial Catalog=FruitBank_DEV;\\\\\"\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- C\\) nincs conn \\(semmi vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"ls -la\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- D\\) Database= prod \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"x Database=MangoManagement;y\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- KESZ\")", + "Bash(cd \"H:/Applications/Mango/Source/FruitBankHybridApp\" && grep -rhi \"DevExpress.Blazor\" --include=\"*.csproj\" . 2>/dev/null | grep -i version | head -5; echo \"---packages.lock/obj---\"; find . -path \"*DevExpress.Blazor*\" -name \"*.nuspec\" 2>/dev/null | head; echo \"---globalpackages---\"; ls ~/.nuget/packages/ 2>/dev/null | grep -i devexpress.blazor | head)" ] } } diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs index 3b319d5f..218952d3 100644 --- a/FruitBank.Common/Entities/EkaerHistory.cs +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -1,9 +1,11 @@ -using AyCode.Core.Serializers.Attributes; +using System.Text.Json.Serialization; +using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Toons; using AyCode.Interfaces.TimeStampInfo; using LinqToDB.Mapping; using Mango.Nop.Core.Entities; + namespace FruitBank.Common.Entities; [AcBinarySerializable(false, true, false, true, false, false)] @@ -19,8 +21,18 @@ public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo [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 = "Lifecycle state of the declaration, stored as int (see EkaerStatus): 0 Pending (auto-created, not yet generated), 1 Generated (tradeCard XML produced and valid), 2 ValidationError (generation produced errors, see ErrorText), 3 Sent (accepted by NAV, EkaerNumber filled), 4 SendError (NAV call failed, see ErrorText).")] + public int StatusId { get; set; } + + /// A enum-nézete. A tárolt érték az int oszlop (StatusId) — a linq2db réteg + /// az enum-property-t nem perzisztálta (az oszlop kimaradt az insertből, a DB default írt 0-t), ezért a nop-minta: + /// int oszlop + nem-mappelt enum wrapper. + [NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, Newtonsoft.Json.JsonIgnore, JsonIgnore] + public EkaerStatus Status + { + get => (EkaerStatus)StatusId; + set => StatusId = (int)value; + } [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; } diff --git a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs index 9e31b37c..74463d40 100644 --- a/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs +++ b/FruitBank.Common/Interfaces/IFruitBankDataControllerCommon.cs @@ -36,6 +36,7 @@ public interface IFruitBankDataControllerCommon public Task UpdateEkaerHistory(EkaerHistory ekaerHistory); public Task GenerateEkaerXmlDocument(int shippingDocumentId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing); + public Task CreateMissingEkaerHistories(DateTime fromDate); #endregion EkaerHistory #region CargoPartner diff --git a/FruitBank.Common/SignalRs/SignalRTags.cs b/FruitBank.Common/SignalRs/SignalRTags.cs index 77e72728..3232da03 100644 --- a/FruitBank.Common/SignalRs/SignalRTags.cs +++ b/FruitBank.Common/SignalRs/SignalRTags.cs @@ -125,6 +125,7 @@ public class SignalRTags : AcSignalRTags public const int UpdateEkaerHistory = 189; public const int GenerateEkaerXmlDocument = 190; public const int CreateEkaerHistory = 191; + public const int CreateMissingEkaerHistories = 192; public const int AuthenticateUser = 195; public const int RefreshToken = 200; diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor index c01fffe2..f6a2e487 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerDetail.razor @@ -3,6 +3,7 @@ @using AyCode.Core.Interfaces @using AyCode.Core.Loggers @using AyCode.Services.Nav.Ekaer.Models +@using FruitBank.Common.Entities @using FruitBankHybrid.Shared.Services.Loggers @using FruitBankHybrid.Shared.Services.SignalRs @@ -27,6 +28,12 @@ } +@if (!string.IsNullOrWhiteSpace(ErrorText)) +{ + @* A generálás/küldés hibái egyben olvashatóan — soronként (a validátor NewLine-nal fűzi). *@ +
@ErrorText
+} + ParentDataItem == null; string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty; + private string? ErrorText => (ParentDataItem as EkaerHistory)?.ErrorText; + private AcObservableCollection TradeCardItems = []; private LoggerClient _logger; diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index be85e3d2..f47d636b 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -12,6 +12,7 @@ @inject IEnumerable LogWriters @inject FruitBankSignalRClient FruitBankSignalRClient +@inject IJSRuntime JS @@ -26,11 +27,44 @@ OnGridFocusedRowChanged="Grid_FocusedRowChanged"> - - + + + @* A kézi NAV-beadás fázisában a Status / EKÁER szám / SentDate kézzel szerkeszthető. *@ + + + + + + @if (!string.IsNullOrWhiteSpace(context.DisplayText)) + { + + @context.DisplayText + + } + + - - + + + + + @{ + var row = (EkaerHistory)context.DataItem; + + + } + + @@ -56,8 +90,29 @@ @if (IsMasterGrid) { - @* EKÁER-rekordot a rendszer hoz létre (auto-rekord) — kézi new/edit/delete tiltva. *@ - + @* EKÁER-rekord a toolbar "sorok létrehozása" gombbal készül (new/delete tiltva — nincs automata + érzékelés, a létrehozás explicit és idempotens). Az Edit a kézi NAV-beadás fázisában + engedélyezett: Status / EKÁER szám / SentDate kézi rögzítéséhez. *@ + + + @* A dátumválasztó editor csak Template-ben mehet toolbarba; a gomb natív DxToolbarItem, + hogy a toolbar stílusát/igazítását kapja (lásd: "Ai process..." minta a GridShippingDocument-ben). *@ + + + + + + + } @@ -110,4 +165,87 @@ FocusedRowVisibleIndex = args.VisibleIndex; EditItemsEnabled = true; } + + private readonly HashSet _generatingIds = []; + + // Elküldött bejelentést nem generálunk újra némán (az már a NAV-nál van — módosítás külön művelet lesz); + // kimenő (Order) generálás még nincs implementálva. + private bool CanGenerate(EkaerHistory ekaerHistory) + => ekaerHistory.Status != EkaerStatus.Sent && !ekaerHistory.IsOutgoing && !_generatingIds.Contains(ekaerHistory.Id); + + // Hibás (validációs hibás) bejelentés XML-jét nem adjuk a user kezébe — ne kerülhessen kézzel a NAV-hoz. + private static bool CanCopy(EkaerHistory ekaerHistory) + => !string.IsNullOrWhiteSpace(ekaerHistory.XmlDoc) && string.IsNullOrWhiteSpace(ekaerHistory.ErrorText); + + private async Task OnGenerateClick(EkaerHistory ekaerHistory) + { + if (!_generatingIds.Add(ekaerHistory.Id)) return; + + try + { + var updated = await FruitBankSignalRClient.GenerateEkaerXmlDocument(ekaerHistory.ForeignKey); + + if (updated == null) + { + _logger.Error($"GenerateEkaerXmlDocument null választ adott; ForeignKey: {ekaerHistory.ForeignKey}"); + return; + } + + // A sor frissítése helyben — a grid ugyanazt a példányt mutatja. + ekaerHistory.Status = updated.Status; + ekaerHistory.XmlDoc = updated.XmlDoc; + ekaerHistory.ErrorText = updated.ErrorText; + ekaerHistory.EkaerNumber = updated.EkaerNumber; + ekaerHistory.SentDate = updated.SentDate; + ekaerHistory.Modified = updated.Modified; + } + catch (Exception ex) + { + _logger.Error($"GenerateEkaerXmlDocument hiba; ForeignKey: {ekaerHistory.ForeignKey}", ex); + } + finally + { + _generatingIds.Remove(ekaerHistory.Id); + Grid?.Reload(); + } + } + + DateTime CreateFromDate { get; set; } = DateTime.Today.AddDays(-3); + private bool _creatingMissing; + + private async Task OnCreateMissingClick() + { + if (_creatingMissing) return; + _creatingMissing = true; + + try + { + var createdCount = await FruitBankSignalRClient.CreateMissingEkaerHistories(CreateFromDate); + _logger.Info($"CreateMissingEkaerHistories; created: {createdCount}; fromDate: {CreateFromDate:yyyy.MM.dd}"); + + if (createdCount > 0) await ReloadDataFromDb(true); + } + catch (Exception ex) + { + _logger.Error($"CreateMissingEkaerHistories hiba; fromDate: {CreateFromDate:yyyy.MM.dd}", ex); + } + finally + { + _creatingMissing = false; + } + } + + private async Task OnCopyClick(EkaerHistory ekaerHistory) + { + if (!CanCopy(ekaerHistory)) return; + + try + { + await JS.InvokeVoidAsync("navigator.clipboard.writeText", ekaerHistory.XmlDoc); + } + catch (Exception ex) + { + _logger.Error($"XmlDoc vágólapra másolása sikertelen; EkaerHistory.Id: {ekaerHistory.Id}", ex); + } + } } diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 8dbe9808..7bad3074 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -81,6 +81,7 @@ namespace FruitBankHybrid.Shared.Services.SignalRs public Task UpdateEkaerHistory(EkaerHistory ekaerHistory) => PostDataAsync(SignalRTags.UpdateEkaerHistory, ekaerHistory); public Task GenerateEkaerXmlDocument(int shippingDocumentId) => GetByIdAsync(SignalRTags.GenerateEkaerXmlDocument, shippingDocumentId); public Task CreateEkaerHistory(int foreignKey, bool isOutgoing) => GetByIdAsync(SignalRTags.CreateEkaerHistory, [foreignKey, isOutgoing]); + public Task CreateMissingEkaerHistories(DateTime fromDate) => GetByIdAsync(SignalRTags.CreateMissingEkaerHistories, fromDate); #endregion EkaerHistory #region CargoPartner diff --git a/FruitBankHybrid.Shared/wwwroot/app.css b/FruitBankHybrid.Shared/wwwroot/app.css index 52abce8f..020e5a8d 100644 --- a/FruitBankHybrid.Shared/wwwroot/app.css +++ b/FruitBankHybrid.Shared/wwwroot/app.css @@ -67,6 +67,20 @@ h1:focus { --icon-ekaer-mask-image: url("images/ekaer-fluent.svg"); } +/* EKÁER toolbar dátumválasztó (GridEkaerHistory) — kompakt szélesség, hogy illeszkedjen a toolbar-elemek közé. */ +.ekaer-create-from-date { + width: 9rem; +} + +/* A Template-es toolbar-elem konténere alapból teljes magasságra nyúlik, és az editor felülre csúszik — + középre igazítjuk, hogy egy vonalban legyen a többi (natív) toolbar-gombbal. */ +.ekaer-create-from-date-item .dxbl-toolbar-item-content, +.ekaer-create-from-date-item .dxbl-toolbar-item-template { + display: flex; + align-items: center; + height: 100%; +} + .icon { width: var(--icon-width); height: var(--icon-height); From f23aebff2d31d3ad3ee5da4fe5e1816707eb0574 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 11 Jun 2026 17:40:33 +0200 Subject: [PATCH 09/17] =?UTF-8?q?EK=C3=81ER=20config=20refactor:=20central?= =?UTF-8?q?ize=20settings,=20add=20audit=20rate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralized EKÁER configuration in new EkaerSettings class (company info, EUR-HUF rate, thresholds). Refactored service and mapper to use EkaerSettings. Added EkaerValueCalculator for HUF value logic. Extended EkaerHistory with ConversionRate for audit. Added IsEkaer to Partner. Updated mapping, VTSZ normalization, UI grid, CSS, and tests. Added diagnostic commands to settings.local.json. --- .claude/settings.local.json | 8 +++- .../Services/Ekaer/FruitBankEkaerService.cs | 26 ++++++++---- FruitBank.Common/Entities/EkaerHistory.cs | 5 +++ FruitBank.Common/Entities/Partner.cs | 3 ++ FruitBank.Common/Interfaces/IPartner.cs | 1 + .../Services/Ekaer/EkaerSettings.cs | 28 +++++++++++++ .../Services/Ekaer/EkaerValueCalculator.cs | 40 +++++++++++++++++++ .../Services/Ekaer/ShippingToEkaerMapper.cs | 30 ++++++++++---- .../Ekaer/ShippingToEkaerMapperTests.cs | 2 +- .../Grids/Ekaers/GridEkaerHistory.razor | 7 +++- FruitBankHybrid.Shared/wwwroot/app.css | 9 +++-- 11 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 FruitBank.Common/Services/Ekaer/EkaerSettings.cs create mode 100644 FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 983a40cc..3d148c8f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -61,7 +61,13 @@ "Skill(update-config)", "Skill(update-config:*)", "PowerShell($script = \"H:\\\\Applications\\\\Mango\\\\Source\\\\FruitBankHybridApp\\\\.claude\\\\hooks\\\\db-dev-guard.ps1\"; \"--- A\\) PROD \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"sqlcmd -S 1.2.3.4 -Q \\\\\"x\\\\\" # Initial Catalog=FruitBank;User ID=sa\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- B\\) DEV \\(semmi vart\\):\"; '{\"tool_name\":\"PowerShell\",\"tool_input\":{\"command\":\"$cs = \\\\\"Data Source=x;Initial Catalog=FruitBank_DEV;\\\\\"\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- C\\) nincs conn \\(semmi vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"ls -la\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- D\\) Database= prod \\(deny vart\\):\"; '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"x Database=MangoManagement;y\"}}' | powershell -NoProfile -ExecutionPolicy Bypass -File $script; \"--- KESZ\")", - "Bash(cd \"H:/Applications/Mango/Source/FruitBankHybridApp\" && grep -rhi \"DevExpress.Blazor\" --include=\"*.csproj\" . 2>/dev/null | grep -i version | head -5; echo \"---packages.lock/obj---\"; find . -path \"*DevExpress.Blazor*\" -name \"*.nuspec\" 2>/dev/null | head; echo \"---globalpackages---\"; ls ~/.nuget/packages/ 2>/dev/null | grep -i devexpress.blazor | head)" + "Bash(cd \"H:/Applications/Mango/Source/FruitBankHybridApp\" && grep -rhi \"DevExpress.Blazor\" --include=\"*.csproj\" . 2>/dev/null | grep -i version | head -5; echo \"---packages.lock/obj---\"; find . -path \"*DevExpress.Blazor*\" -name \"*.nuspec\" 2>/dev/null | head; echo \"---globalpackages---\"; ls ~/.nuget/packages/ 2>/dev/null | grep -i devexpress.blazor | head)", + "PowerShell($cs = \"Data Source=100.73.220.50;Initial Catalog=FruitBank_DEV;Integrated Security=False;User ID=sa;Password=v6f_?xNfg9N1;TrustServerCertificate=True\"; $conn = New-Object System.Data.SqlClient.SqlConnection\\($cs\\); $conn.Open\\(\\); $cmd = $conn.CreateCommand\\(\\); $cmd.CommandText = \"SELECT Id, ForeignKey, StatusId, LEFT\\(ISNULL\\(ErrorText,''\\), 80\\) AS Err, CASE WHEN XmlDoc LIKE '%%' THEN SUBSTRING\\(XmlDoc, CHARINDEX\\('', XmlDoc\\) + 17, 40\\) ELSE '\\(nincs destinationName elem\\)' END AS DestName, LEN\\(XmlDoc\\) AS XmlLen, Modified FROM dbo.fbEkaerHistory WHERE Id IN \\(1,2\\) ORDER BY Id\"; $r = $cmd.ExecuteReader\\(\\); while \\($r.Read\\(\\)\\) { \"Id=$\\($r['Id']\\) FK=$\\($r['ForeignKey']\\) StatusId=$\\($r['StatusId']\\) XmlLen=$\\($r['XmlLen']\\) Modified=$\\($r['Modified']\\)\"; \" DestName: $\\($r['DestName']\\)\"; \" Err: $\\($r['Err']\\)\"; \"---\" }; $r.Close\\(\\); $conn.Close\\(\\))", + "Bash(find /h/Applications/Mango/Source -iname \"appsettings*.json\" -path \"*Nop.Web*\" 2>/dev/null | head -20)", + "Bash(grep -c -a \"ConversionRate\" FruitBank.Common.dll)", + "Bash(grep -c -a \"ConversionRate\" FruitBank.Common.Server.dll)", + "Bash(grep -c -a \"EurHufRate\" FruitBank.Common.dll FruitBank.Common.Server.dll)", + "Bash(ls -la --time-style=+%H:%M FruitBank.Common.dll FruitBank.Common.Server.dll)" ] } } diff --git a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs index c5384fb4..9e21f0b8 100644 --- a/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs +++ b/FruitBank.Common.Server/Services/Ekaer/FruitBankEkaerService.cs @@ -9,22 +9,22 @@ namespace FruitBank.Common.Server.Services.Ekaer; /// /// /// A teljes lánc: map (, FruitBank.Common) → -/// validate → send (, AyCode.Services). A NAV-fiók hitelesítő adatait -/// (INavCredentials) és a saját cégadatokat () a DI szolgáltatja. +/// validate → send (, AyCode.Services). A saját cégadatot +/// () és a NAV-fiók hitelesítő adatait a DI szolgáltatja. /// public sealed class FruitBankEkaerService : IFruitBankEkaerService { private readonly IShippingToEkaerMapper _mapper; private readonly IEkaerSubmitService _submitService; private readonly IEkaerTradeCardValidator _validator; - private readonly EkaerCompanyInfo _company; + private readonly EkaerSettings _settings; - public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerCompanyInfo company) + public FruitBankEkaerService(IShippingToEkaerMapper mapper, IEkaerSubmitService submitService, IEkaerTradeCardValidator validator, EkaerSettings settings) { _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _submitService = submitService ?? throw new ArgumentNullException(nameof(submitService)); _validator = validator ?? throw new ArgumentNullException(nameof(validator)); - _company = company ?? throw new ArgumentNullException(nameof(company)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } public Task SubmitShippingAsync(Shipping shipping, OperationType operation = OperationType.Create, CancellationToken cancellationToken = default) @@ -32,7 +32,7 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService ArgumentNullException.ThrowIfNull(shipping); // map (FruitBank.Common) → submit: validate → send (AyCode.Services) - var operations = _mapper.MapShipping(shipping, _company, operation); + var operations = _mapper.MapShipping(shipping, _settings.Company, operation); return _submitService.SubmitAsync(operations, cancellationToken); } @@ -42,17 +42,29 @@ public sealed class FruitBankEkaerService : IFruitBankEkaerService ekaerHistory ??= new EkaerHistory { ForeignKey = document.Id, IsOutgoing = false }; + // Config-ellenőrzés a leképezés ELŐTT: külföldi (nem HUF) feladónál az árfolyam kötelező — különben a + // value csendben 0 lenne. Hibás configgal nem generálunk félrevezető XML-t, hanem jelezzük a hibát. + if (!EkaerValueCalculator.IsHuf(document.Partner?.Currency) && _settings.EurHufRate <= 0) + { + ekaerHistory.Status = EkaerStatus.ValidationError; + ekaerHistory.ErrorText = "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — a tétel-érték nem számolható."; + return ekaerHistory; + } + var operation = new TradeCardOperationType { Index = 1, Operation = OperationType.Create, - TradeCard = _mapper.MapDocument(document, _company), + TradeCard = _mapper.MapDocument(document, _settings.Company), }; var errors = _validator.Validate(operation); // Az XML validációs hibánál IS tárolódik — a detail-nézetben így látszik, mi hiányzik. ekaerHistory.XmlDoc = NavXmlHelper.Serialize(operation.TradeCard); + // Audit: a value-számításhoz TÉNYLEGESEN alkalmazott árfolyam (HUF → 1, külföldi → FX-ráta) — pont az, + // amivel a value készült. A NAV-sémában nincs árfolyam-mező, csak a HUF value; ez az oszlop őrzi meg, hogyan. + ekaerHistory.ConversionRate = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate); ekaerHistory.Status = errors.Count == 0 ? EkaerStatus.Generated : EkaerStatus.ValidationError; ekaerHistory.ErrorText = errors.Count == 0 ? null : string.Join(Environment.NewLine, errors.Select(e => e.ErrorMessage)); diff --git a/FruitBank.Common/Entities/EkaerHistory.cs b/FruitBank.Common/Entities/EkaerHistory.cs index 218952d3..204c3a81 100644 --- a/FruitBank.Common/Entities/EkaerHistory.cs +++ b/FruitBank.Common/Entities/EkaerHistory.cs @@ -2,6 +2,7 @@ using AyCode.Core.Serializers.Attributes; using AyCode.Core.Serializers.Toons; using AyCode.Interfaces.TimeStampInfo; +using LinqToDB; using LinqToDB.Mapping; using Mango.Nop.Core.Entities; @@ -37,6 +38,10 @@ public sealed class EkaerHistory: MgEntityBase, ITimeStampInfo [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; } + [Column(DataType = DataType.DecFloat)] + [ToonDescription(Purpose = "The conversion rate actually applied when computing this declaration's item values to HUF: 1 for domestic (HUF) suppliers (no conversion), the FX rate (e.g. EUR→HUF) for foreign suppliers. Null before the first Generate. Currency-agnostic by design — works for any source currency. Audit trail: the invoice amount times this rate yields the HUF value in the tradeCard; the NAV schema has no rate field, so this column preserves how the value was derived.")] + public double? ConversionRate { 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; } diff --git a/FruitBank.Common/Entities/Partner.cs b/FruitBank.Common/Entities/Partner.cs index 45d4205e..d8e492ce 100644 --- a/FruitBank.Common/Entities/Partner.cs +++ b/FruitBank.Common/Entities/Partner.cs @@ -11,6 +11,9 @@ namespace FruitBank.Common.Entities; [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)] public sealed class Partner : PartnerBase, IPartner { + [ToonDescription(Purpose = "Whether shipments from this supplier are subject to NAV EKÁER reporting. Default true; set false for partners that do not require EKÁER (e.g. multiple companies sharing a wholesale-market address where no road transport between them occurs).")] + public bool IsEkaer { get; set; } = true; + [Association(ThisKey = nameof(Id), OtherKey = nameof(PartnerDepot.PartnerId), CanBeNull = true)] public List? PartnerDepots { get; set; } diff --git a/FruitBank.Common/Interfaces/IPartner.cs b/FruitBank.Common/Interfaces/IPartner.cs index 953fc33a..5dec94af 100644 --- a/FruitBank.Common/Interfaces/IPartner.cs +++ b/FruitBank.Common/Interfaces/IPartner.cs @@ -13,6 +13,7 @@ public interface ICargoPartner : IPartnerBase public interface IPartner : IPartnerBase { + bool IsEkaer { get; set; } List? PartnerDepots { get; set; } List? ShippingDocuments { get; set; } } diff --git a/FruitBank.Common/Services/Ekaer/EkaerSettings.cs b/FruitBank.Common/Services/Ekaer/EkaerSettings.cs new file mode 100644 index 00000000..94d7b5e4 --- /dev/null +++ b/FruitBank.Common/Services/Ekaer/EkaerSettings.cs @@ -0,0 +1,28 @@ +using AyCode.Services.Nav.Ekaer; + +namespace FruitBank.Common.Services.Ekaer; + +/// +/// A teljes EKÁER konfiguráció egy helyen (configból, appsettings.json „Ekaer" szekció): a bejelentő +/// cégadata + a küszöbök + az árfolyam. A küszöbök és az árfolyam évente / jogszabály szerint változhatnak, +/// ezért configban élnek — nem a kódban beégetve. +/// +public sealed class EkaerSettings +{ + /// A bejelentő saját cégadatai (címzett a bejövő relációban) + a lerakodási hely. + public EkaerCompanyInfo Company { get; set; } = new(); + + /// EUR→HUF átváltási árfolyam a tétel-érték HUF-ra számításához. Forrás: MNB napi középárfolyam + /// (a NAV nem közöl külön EKÁER-árfolyamot). SZÁNDÉKOSAN nincs default: ha a config nem töltődik be, 0 marad, + /// és a külföldi érték-átváltás hibát dob () — így elavult / + /// beégetett árfolyammal soha nem számolunk. + public double EurHufRate { get; set; } + + /// Tömeg-küszöb kg-ban: e felett (vagy az érték-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 200 kg. + /// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé). + public double ThresholdWeightKg { get; set; } + + /// Érték-küszöb HUF-ban (nettó): e felett (vagy a tömeg-küszöb felett) kell EKÁER. Kockázatos élelmiszer: 250 000 Ft. + /// Default nélkül: be nem töltött config → 0 → minden szállítmány „átlépi" (mindent jelentünk, a biztonság felé). + public long ThresholdValueHuf { get; set; } +} diff --git a/FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs b/FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs new file mode 100644 index 00000000..d028f688 --- /dev/null +++ b/FruitBank.Common/Services/Ekaer/EkaerValueCalculator.cs @@ -0,0 +1,40 @@ +using FruitBank.Common.Entities; + +namespace FruitBank.Common.Services.Ekaer; + +/// +/// A tétel-érték HUF-ra számítása — közös a leképezés (tradeCardItem.value) és a küszöb-kapu +/// (CreateMissingEkaerHistories) között, hogy a kétféle számítás ne csúszhasson el egymástól. +/// +/// +/// A forrás-pénznem a Partner.Currency (ISO 4217). Jelenleg csak HUF és EUR van használatban +/// (100%-ban a partnerből következik): HUF → nincs átváltás, minden más → EUR-ként, az árfolyammal. +/// Ha valaha más deviza is megjelenik, ezt a feltevést bővíteni kell (akkor per-deviza árfolyam kell). +/// +public static class EkaerValueCalculator +{ + public static bool IsHuf(string? currency) => string.Equals(currency?.Trim(), "HUF", StringComparison.OrdinalIgnoreCase); + + /// A forrás-pénznem → HUF szorzó. HUF → 1 (árfolyam nem kell). Minden más (EUR) → a megadott + /// EUR-HUF árfolyam; ha az hiányzik/0, — soha nem számolunk elavult / + /// üres árfolyammal (a config-default szándékosan nincs). + public static double ResolveRateToHuf(string? currency, double eurHufRate) + { + if (IsHuf(currency)) return 1d; + if (eurHufRate <= 0) + throw new InvalidOperationException( + "EKÁER EUR-HUF árfolyam nincs konfigurálva (appsettings Ekaer:ExchangeRate:EurHuf) — külföldi (nem HUF) értéket nem lehet HUF-ra számolni."); + return eurHufRate; + } + + /// Egy tétel értéke a számla pénznemében (egységár × dokumentum-mennyiség), átváltás előtt. + public static double ItemLineValue(ShippingItem item) => item.UnitPriceOnDocument * item.QuantityOnDocument; + + /// Egy tétel értéke HUF-ban, egészre kerekítve. A NAV value > 0-t vár — 0/ismeretlen + /// esetén null (a mezőt nem töltjük, a validátor jelzi, ha kötelező lenne). + public static long? ItemValueHuf(ShippingItem item, double rateToHuf) + { + var huf = Math.Round(ItemLineValue(item) * rateToHuf, MidpointRounding.AwayFromZero); + return huf > 0 ? (long)huf : null; + } +} diff --git a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs index 7b221ff3..f2230597 100644 --- a/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs +++ b/FruitBank.Common/Services/Ekaer/ShippingToEkaerMapper.cs @@ -19,6 +19,11 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper /// A NAV EKÁER magyar rendszer — a „belföld" mindig HU; minden más feladó-ország import. private const string HomeCountry = "HU"; + private readonly EkaerSettings _settings; + + public ShippingToEkaerMapper(EkaerSettings settings) + => _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + public IReadOnlyList MapShipping(Shipping shipping, EkaerCompanyInfo company, OperationType operation = OperationType.Create) { ArgumentNullException.ThrowIfNull(shipping); @@ -31,11 +36,12 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper foreach (var document in shipping.ShippingDocuments ?? []) { index++; + var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate); operations.Add(new TradeCardOperationType { Index = index, Operation = operation, - TradeCard = BuildTradeCard(shipping, document, company), + TradeCard = BuildTradeCard(shipping, document, company, rateToHuf), }); } @@ -47,10 +53,11 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(company); - return BuildTradeCard(document.Shipping, document, company); + var rateToHuf = EkaerValueCalculator.ResolveRateToHuf(document.Partner?.Currency, _settings.EurHufRate); + return BuildTradeCard(document.Shipping, document, company, rateToHuf); } - private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company) + private static TradeCardType BuildTradeCard(Shipping? shipping, ShippingDocument document, EkaerCompanyInfo company, double rateToHuf) { var seller = document.Partner; // a beszállító (feladó) — ICompanyInfoBase @@ -83,7 +90,7 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper if (shipping?.CargoTruck != null) tradeCard.Vehicle = BuildVehicle(shipping.CargoTruck); if (shipping?.CargoTrailer != null) tradeCard.Vehicle2 = BuildVehicle(shipping.CargoTrailer); - foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item)); + foreach (var item in document.ShippingItems ?? []) tradeCard.Items.Add(BuildItem(item, rateToHuf)); return tradeCard; } @@ -96,15 +103,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper ? TradeType.D : TradeType.I; - private static TradeCardItemType BuildItem(ShippingItem item) => new() + private static TradeCardItemType BuildItem(ShippingItem item, double rateToHuf) => new() { ItemExternalId = item.Id.ToString(), // Bejövő áru = beszerzés → A. (Enum: S=értékesítés, A=beszerzés, W=bérmunka, O=egyéb.) Lásd EKAER_TODO #9. TradeReason = TradeReasonType.A, - ProductVtsz = item.ProductDto?.Gtin, // VTSZ — átmenetileg a Gtin oszlopban (FBANKAPP-DMODEL-I-P6X4) + ProductVtsz = NormalizeVtsz(item.ProductDto?.Gtin), // VTSZ — átmenetileg a Gtin oszlopban (FBANKAPP-DMODEL-I-P6X4) ProductName = item.ProductName, Weight = (decimal)item.MeasuredGrossWeight, // bruttó tömeg kg-ban (lásd EKAER_TODO #4) - // Value (HUF): a deviza/FX tisztázásáig NEM töltjük (a mező opcionális). Lásd EKAER_TODO #3, #10. + Value = EkaerValueCalculator.ItemValueHuf(item, rateToHuf), // beszerzési érték HUF-ban (Partner.Currency → árfolyam); 0/ismeretlen → null }; private static BasicVehicleDetailType BuildVehicle(CargoTruck truck) => new() @@ -163,6 +170,15 @@ public sealed class ShippingToEkaerMapper : IShippingToEkaerMapper return Truncate(EmptyToNull(cleaned), 10); } + /// VTSZ normalizálása. Pattern: [0-9]{4,8} — a forrásban tagolt formátum is lehet + /// („0805 10 00", „0805.10.00"), ezért csak a számjegyek maradnak; a 4-nél rövidebbet a validátor jelzi. + private static string? NormalizeVtsz(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return null; + var cleaned = new string([.. value.Where(char.IsAsciiDigit)]); + return Truncate(EmptyToNull(cleaned), 8); + } + private static string? EmptyToNull(string? value) => string.IsNullOrEmpty(value) ? null : value; private static string? Truncate(string? value, int maxLen) diff --git a/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs index 52b441fe..461ab4c9 100644 --- a/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs +++ b/FruitBankHybrid.Shared.Tests/Ekaer/ShippingToEkaerMapperTests.cs @@ -15,7 +15,7 @@ namespace FruitBankHybrid.Shared.Tests.Ekaer; [TestClass] public sealed class ShippingToEkaerMapperTests { - private static readonly ShippingToEkaerMapper Mapper = new(); + private static readonly ShippingToEkaerMapper Mapper = new(new EkaerSettings()); // ---- Helpers ------------------------------------------------------------ diff --git a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor index f47d636b..de00d48b 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Ekaers/GridEkaerHistory.razor @@ -33,6 +33,8 @@ + @* Audit: a value-számításhoz alkalmazott árfolyam (HUF feladónál 1). ReadOnly — generáláskor töltődik. *@ + @if (!string.IsNullOrWhiteSpace(context.DisplayText)) @@ -97,7 +99,9 @@ @* A dátumválasztó editor csak Template-ben mehet toolbarba; a gomb natív DxToolbarItem, hogy a toolbar stílusát/igazítását kapja (lásd: "Ai process..." minta a GridShippingDocument-ben). *@ - + @* dxbl-toolbar-edit: a DevExpress téma saját opt-in osztálya editor-t hordozó toolbar-itemre — + ettől kapja a beépített középre igazítást és a toolbarba simuló editor-stílust. *@ +