Compare commits

...

10 Commits

Author SHA1 Message Date
Loretta ba05b5f37e Add bunit & Newtonsoft.Json refs, add (commented) test stubs
Added bunit and Newtonsoft.Json package references to the test project for Blazor component and JSON testing support. Introduced two new test files, GridPartnerBaseTests.cs and GridPartnerRazorTests.cs, containing commented-out MSTest and bUnit test stubs for GridPartnerBase and GridPartner components. No active tests are enabled yet.
2025-12-13 12:31:46 +01:00
Loretta 62e8294448 Initialize SerializerOptions in DevAdminSignalRHub ctor
Set SerializerOptions to AcBinarySerializerOptions in the DevAdminSignalRHub constructor, enabling configurable serialization (binary or JSON) for SignalR communication. Includes a commented option for AcJsonSerializerOptions. No other logic affected.
2025-12-13 00:16:41 +01:00
Loretta fdcd47fa75 Update UI access from Developer to Administrator role
Replaced all checks and UI visibility conditions using LoggedInModel.IsDeveloper with LoggedInModel.IsAdministrator across Blazor components and grid templates. This change restricts advanced features and UI elements to administrator users instead of developer users. The "Delete" toolbar item remains accessible to developers. No other logic or functionality was modified.
2025-12-13 00:16:34 +01:00
Loretta 056a69ecc8 Update local HTTPS, layout auth, deps, and add SignalR tests
- Switch FruitBankConstClient.BaseUrl to HTTPS for localhost.
- Pin DevExpress.Blazor to 25.1.3 in client project.
- Add Microsoft.Extensions.ObjectPool v9 to test project for SignalR.
- Reformat App.razor markup; comment out ResourcePreloader.
- Render @Body only if logged in or on login page in MainLayout.
- Redirect to /Login if not authenticated after auto-login.
- Add SandboxEndpointSimpleTests.cs for HTTP/SignalR endpoint tests against local sandbox.
2025-12-11 23:46:20 +01:00
Loretta 0d9ced990a Add secure cross-platform auto-login with credential storage
Introduces ISecureCredentialService abstraction and platform-specific implementations for secure credential storage (WebAssembly, MAUI, server). Refactors LoggedInModel to support async auto-login, login, and logout using stored credentials. Updates DI and UI logic to enable seamless auto-login and logout across all platforms. Cleans up redundant navigation checks and improves maintainability.
2025-12-09 16:46:47 +01:00
Loretta 4ef318973f Add grid sync state tracking and robust login redirection
Introduce IsSyncing and OnSyncingStateChanged to IMgGridBase and MgGridBase for real-time sync state tracking and event notification. Update FruitBankToolbarTemplate to enable/disable the reload button based on grid sync and reload state, subscribing to sync events and cleaning up on disposal. Implement IAsyncDisposable in MgGridBase to prevent memory leaks. Update login navigation to use forceLoad for reliability. These changes improve UI responsiveness and resource management.
2025-12-09 11:27:21 +01:00
Loretta 346d433196 Refactor, enhance, and improve test coverage
Refactored `OnDataSourceLoaded` to be asynchronous for better state handling. Downgraded `Newtonsoft.Json` to version `13.0.3` across multiple projects for compatibility. Enhanced `MeasuringItemPalletBase` with `SetParentPropToNull` and `SetForeignKey` methods. Refactored `OrderItemPallet`, `ShippingItemPallet`, and `StockTakingItemPallet` to simplify table attributes and improve parent-child relationship handling.

Added `IsReadyForClose` to `StockTaking` for better closure validation. Updated SignalR tag constants to reflect new functionality. Improved Razor components (`PalletItemComponent`, `StockTakingTemplate`, `MeasuringIn`, `MeasuringOut`) to streamline logic and maintain proper references.

Introduced `JsonExtensionTests` for comprehensive validation of JSON serialization/deserialization, including deep hierarchies, circular references, and hybrid references. Added `test_debug.ps1` for streamlined test debugging. Performed general code cleanup and improved test coverage.
2025-12-08 15:50:57 +01:00
Loretta 1b6aae83f1 StockTaking in progress... 2025-12-04 13:52:51 +01:00
Loretta e13e32dc57 .Net10, VS2026; StockTaking in progress... 2025-12-01 16:18:47 +01:00
Loretta 7e4d0a85e8 Upgrade to .net10 and Visual Studio 2026; StockTaking in progress... 2025-11-28 08:15:58 +01:00
73 changed files with 2854 additions and 375 deletions

View File

@ -10,11 +10,12 @@
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.11" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.11" />
<!--<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />-->
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
@ -38,7 +39,7 @@
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.Server.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AspNetCore.SignalR.Core">
<HintPath>C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\9.0.10\ref\net9.0\Microsoft.AspNetCore.SignalR.Core.dll</HintPath>
<HintPath>C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\9.0.11\ref\net9.0\Microsoft.AspNetCore.SignalR.Core.dll</HintPath>
</Reference>
<Reference Include="Mango.Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>

View File

@ -0,0 +1,8 @@
using FruitBank.Common.Interfaces;
namespace FruitBank.Common.Server.Interfaces;
public interface IStockSignalREndpointServer : IStockSignalREndpointCommon
{
}

View File

@ -1,4 +1,5 @@
using AyCode.Core.Loggers;
using AyCode.Core.Extensions;
using AyCode.Core.Loggers;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.SignalRs;
using FruitBank.Common.Interfaces;
@ -29,11 +30,15 @@ namespace FruitBank.Common.Server.Services.SignalRs;
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
{
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IEnumerable<IAcLogWriterBase> logWriters)
ICustomOrderSignalREndpointServer customOrderSignalREndpoint, IStockSignalREndpointServer stockSignalREndpointServer, IEnumerable<IAcLogWriterBase> logWriters)
: base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
{
SerializerOptions = new AcBinarySerializerOptions();
//SerializerOptions = new AcJsonSerializerOptions();
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(fruitBankDataController));
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(customOrderSignalREndpoint));
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(stockSignalREndpointServer));
}
protected override void LogContextUserNameAndId()

View File

@ -13,6 +13,7 @@ using Nop.Core;
//using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
@ -21,7 +22,7 @@ namespace FruitBank.Common.Dtos;
public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
{
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
private static Expression<Func<OrderDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderDto, genericAttributeDto) =>
orderDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(Order);
@ -29,21 +30,21 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
[Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttribute.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = true)]
public List<GenericAttributeDto> GenericAttributes { get; set; }
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsMeasured
{
get => IsMeasuredAndValid();
set => throw new Exception($"OrderDto.IsMeasured not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsMeasurable
{
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
set => throw new Exception($"OrderDto.IsMeasurable not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public string TimeOfReceiptText
{
get
@ -60,25 +61,25 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
}
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsAllOrderItemAudited => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public MeasuringStatus MeasuringStatus
{
get
@ -99,7 +100,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
public OrderDto(Order order) : base(order)
{ }
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsComplete => OrderStatus == OrderStatus.Complete;
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Nop.Core;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
@ -15,7 +16,7 @@ namespace FruitBank.Common.Dtos;
public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
{
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
private static Expression<Func<OrderItemDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) =>
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(OrderItem);
@ -29,28 +30,28 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
[Association(ThisKey = nameof(OrderId), OtherKey = nameof(OrderDto.Id), CanBeNull = false)]
public OrderDto OrderDto { get; set; }
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsMeasured
{
get => IsMeasuredAndValid();
set => throw new Exception($"OrderItemDto.IsMeasured not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsMeasurable
{
get => ProductDto!.IsMeasurable;
set => throw new Exception($"OrderItemDto.IsMeasurable not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int TrayQuantity
{
get => OrderItemPallets.Sum(x => x.TrayQuantity);
set => throw new Exception($"OrderItemDto.TrayQuantity not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double NetWeight
{
get
@ -67,7 +68,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double GrossWeight
{
get
@ -84,20 +85,20 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double AverageWeight => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d;
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double AverageWeightDifference => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0;
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool AverageWeightIsValid => !IsMeasurable ||
(ProductDto!.AverageWeight > 0 && ((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsAudited => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited);
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public MeasuringStatus MeasuringStatus
{
get

View File

@ -2,17 +2,18 @@
using LinqToDB.Mapping;
using Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Extensions;
using Mango.Nop.Core.Interfaces.ForeignKeys;
using Newtonsoft.Json;
//using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
using Mango.Nop.Core.Interfaces.ForeignKeys;
namespace FruitBank.Common.Dtos;
public class ProductDto : MgProductDto, IProductDto
{
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
private static Expression<Func<ProductDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) =>
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == "Product";// nameof(Product);
@ -27,7 +28,7 @@ public class ProductDto : MgProductDto, IProductDto
//public ProductDto(Product product) : base(product)
//{ }
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsMeasurable
{
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
@ -41,7 +42,7 @@ public class ProductDto : MgProductDto, IProductDto
}
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double Tare
{
get => GenericAttributes.GetValueOrDefault<double>(nameof(ITare.Tare));
@ -49,14 +50,14 @@ public class ProductDto : MgProductDto, IProductDto
set => throw new Exception($"ProductDto.Tare not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double NetWeight
{
get => GenericAttributes.GetValueOrDefault<double>(nameof(IMeasuringNetWeight.NetWeight));
set => throw new Exception($"ProductDto.NetWeight not set");
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int IncomingQuantity
{
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
@ -70,13 +71,13 @@ public class ProductDto : MgProductDto, IProductDto
//}
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int AvailableQuantity => StockQuantity + IncomingQuantity;
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double AverageWeightTreshold => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeightTreshold));
public bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using Nop.Core.Domain.Catalog;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -15,28 +16,28 @@ namespace FruitBank.Common.Dtos
{
public class StockQuantityHistoryDto : MgStockQuantityHistoryDto<ProductDto>, IStockQuantityHistoryDto
{
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int? StockQuantityHistoryId
{
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
set => StockQuantityHistoryExt!.StockQuantityHistoryId = value!.Value;
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double? NetWeightAdjustment
{
get => StockQuantityHistoryExt?.NetWeightAdjustment;
set => StockQuantityHistoryExt!.NetWeightAdjustment = value;
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double? NetWeight
{
get => StockQuantityHistoryExt?.NetWeight;
set => StockQuantityHistoryExt!.NetWeight = value;
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsInconsistent
{
get => StockQuantityHistoryExt?.IsInconsistent ?? false;

View File

@ -35,7 +35,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
set => _palletWeight = double.Round(value, 0);
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double NetWeight
{
get => CalculateNetWeight();
@ -59,8 +59,12 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public virtual MeasuringStatus MeasuringStatus => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted;
public abstract void SetParentPropToNull();
public void SetForeignKey(int foreignKey) => ForeignItemId = foreignKey;
public virtual double CalculateNetWeight() => double.Round(GrossWeight - PalletWeight - (TareWeight * TrayQuantity), 1);

View File

@ -4,6 +4,9 @@ using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Newtonsoft.Json;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations.Schema;
using Column = LinqToDB.Mapping.ColumnAttribute;
using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
@ -20,9 +23,11 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
public int RevisorId { get; set; }
public bool IsAudited => RevisorId > 0;
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[Association(ThisKey = nameof(OrderItemId), OtherKey = nameof(OrderItemDto.Id), CanBeNull = true)]
public OrderItemDto? OrderItemDto { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public override MeasuringStatus MeasuringStatus => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus;
public override double CalculateNetWeight() => base.CalculateNetWeight();
@ -31,8 +36,9 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
return OrderItemId > 0 && base.IsValidSafeMeasuringValues();
}
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
/// <summary>
/// "Szigorúbb" mint az IsValidSafeMeasuringValues()
/// </summary>
@ -42,4 +48,9 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
{
return OrderItemId > 0 && base.IsValidMeasuringValues(isMeasurable);
}
public override void SetParentPropToNull()
{
OrderItemDto = null;
}
}

View File

@ -2,6 +2,7 @@
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Newtonsoft.Json;
namespace FruitBank.Common.Entities;
@ -14,7 +15,7 @@ public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
public int DocumentTypeId { get; set; }
[NotColumn, NotMapped]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public DocumentType DocumentType
{
get => (DocumentType)DocumentTypeId;

View File

@ -1,16 +1,19 @@
using AyCode.Core.Interfaces;
using FruitBank.Common.Dtos;
using FruitBank.Common.Enums;
using FruitBank.Common.Interfaces;
using LinqToDB;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Newtonsoft.Json;
using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
//using Nop.Core.Domain.Catalog;
using DataType = LinqToDB.DataType;
using FruitBank.Common.Dtos;
using FruitBank.Common.Enums;
using Newtonsoft.Json;
using Column = LinqToDB.Mapping.ColumnAttribute;
using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
@ -29,7 +32,7 @@ public class ShippingItem : MgEntityBase, IShippingItem
/// <summary>
/// get => ProductDto?.Name ?? Name
/// </summary>
[NotColumn, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public string ProductName => ProductDto?.Name ?? Name;
public int PalletsOnDocument { get; set; }
@ -79,6 +82,7 @@ public class ShippingItem : MgEntityBase, IShippingItem
[SkipValuesOnUpdate] public DateTime Created { get; set; }
public DateTime Modified { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public MeasuringStatus MeasuringStatus
{
get

View File

@ -1,11 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography.X509Certificates;
using FruitBank.Common.Interfaces;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Security.Cryptography.X509Certificates;
namespace FruitBank.Common.Entities;
[Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
{
@ -15,6 +16,7 @@ public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
set => ForeignItemId = value;
}
//[Newtonsoft.Json.JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[LinqToDB.Mapping.Association(ThisKey = nameof(ShippingItemId), OtherKey = nameof(ShippingItem.Id), CanBeNull = true)]
public ShippingItem? ShippingItem { get; set; }
@ -34,4 +36,9 @@ public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
{
return ShippingItemId > 0 && base.IsValidMeasuringValues(isMeasurable);
}
public override void SetParentPropToNull()
{
ShippingItem = null;
}
}

View File

@ -0,0 +1,15 @@
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
public class StockTaking : MgStockTaking<StockTakingItem>
{
public override bool IsReadyForClose()
{
if (StockTakingItems == null || StockTakingItems.Count == 0) return false;
return StockTakingItems.Where(stockTakingItem => stockTakingItem is { IsRequiredForMeasuring: true, IsInvalid: false }).All(x => x.IsMeasured);
}
}

View File

@ -0,0 +1,55 @@
using FruitBank.Common.Dtos;
using LinqToDB;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
using Column = LinqToDB.Mapping.ColumnAttribute;
using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
{
public bool IsMeasurable { get; set; }
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
public double OriginalNetWeight { get; set; }
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
public double MeasuredNetWeight { get; set; }
public int InProcessOrdersQuantity { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d;
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockTakingItemPallet.StockTakingItemId), CanBeNull = true)]
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public bool IsInvalid => TotalOriginalQuantity < 0;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
public string DisplayText
{
get
{
if (IsInvalid) return $"[HIBA] {Product!.Name}";
if (IsMeasured) return $"[KÉSZ] {Product!.Name}";
return IsRequiredForMeasuring ? $"[KÖT] {Product!.Name}" : $"{Product!.Name}";
}
}
}

View File

@ -0,0 +1,46 @@
using FruitBank.Common.Dtos;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using System.ComponentModel.DataAnnotations.Schema;
namespace FruitBank.Common.Entities;
public interface IStockTakingItemPallet : IMeasuringItemPalletBase
{
int StockTakingItemId { get; set; }
public StockTakingItem? StockTakingItem{ get; set; }
}
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
{
public int StockTakingItemId
{
get => ForeignItemId;
set => ForeignItemId = value;
}
//[Newtonsoft.Json.JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[Association(ThisKey = nameof(StockTakingItemId), OtherKey = nameof(StockTakingItem.Id), CanBeNull = true)]
public StockTakingItem? StockTakingItem { get; set; }
public override double CalculateNetWeight() => base.CalculateNetWeight();
public override bool IsValidSafeMeasuringValues()
{
return StockTakingItemId > 0 && TrayQuantity >= 0 && TareWeight >= 0 && PalletWeight >= 0 && NetWeight >= 0 && GrossWeight >= 0;
}
public override bool IsValidMeasuringValues(bool isMeasurable)
{
return StockTakingItemId > 0 && TrayQuantity >= 0 && ((!isMeasurable && NetWeight == 0 && GrossWeight == 0 && PalletWeight == 0 && TareWeight == 0)
|| (isMeasurable && NetWeight >= 0 && GrossWeight >= 0 && PalletWeight >= 0 && TareWeight >= 0));
}
public override void SetParentPropToNull()
{
StockTakingItem = null;
}
}

View File

@ -11,8 +11,8 @@
<PackageReference Include="linq2db.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,4 @@
using AyCode.Core.Consts;
using AyCode.Core.Consts;
using AyCode.Core.Loggers;
namespace FruitBank.Common;
@ -14,7 +14,7 @@ public static class FruitBankConstClient
// public static string BaseUrl = "https://shop.fruitbank.hu"; //FrutiBank nop
#endif
//public static string BaseUrl = "http://localhost:5000"; //FrutiBank nop
//public static string BaseUrl = "http://localhost:59579"; //FrutiBank nop
//public static string BaseUrl = "http://10.0.2.2:59579"; //FrutiBank (android) nop
//public static string BaseUrl = "https://localhost:7144"; //HybridApp
@ -39,6 +39,11 @@ public static class FruitBankConstClient
public const string StockQuantityHistoryExtDbTableName = "fbStockQuantityHistoryExt";
public const string StockTakingDbTableName = "fbStockTaking";
public const string StockTakingItemDbTableName = "fbStockTakingItem";
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
//public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
//public static Guid[] SysAdmins = new Guid[3]
@ -107,3 +112,5 @@ public static string SystemEmailAddress = "test@touriam.com";
public static LogLevel DefaultLogLevelClient = LogLevel.Detail;
#endif
}

View File

@ -2,6 +2,7 @@
using FruitBank.Common.Entities;
using FruitBank.Common.Models;
using Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Models;
using Nop.Core.Domain.Customers;
@ -72,4 +73,11 @@ public interface IFruitBankDataControllerCommon
Task<MgLoginModelResponse?> LoginMeasuringUser(MgLoginModelRequest loginModelRequest);
Task<List<Partner>?> ProcessAndSaveFullShippingJson(string fullShippingJson, int customerId);
public Task<List<GenericAttributeDto>?> GetGenericAttributeDtosByEntityIdAndKeyGroup(int productId, string keyGroup, int storeId);
public Task<GenericAttributeDto?> AddGenericAttributeDto(GenericAttributeDto genericAttributeDto);
public Task<GenericAttributeDto?> UpdateGenericAttributeDto(GenericAttributeDto genericAttributeDto);
}

View File

@ -14,6 +14,8 @@ public interface IMeasuringItemPalletBase : IEntityInt, IMeasuringValues, IMeasu
int? CreatorId { get; set; }
int? ModifierId { get; set; }
void SetParentPropToNull();
double CalculateNetWeight();
bool IsValidSafeMeasuringValues();

View File

@ -0,0 +1,6 @@
namespace FruitBank.Common.Interfaces;
public interface IStockSignalREndpointClient : IStockSignalREndpointCommon
{
}

View File

@ -0,0 +1,24 @@
using FruitBank.Common.Entities;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Interfaces;
public interface IStockSignalREndpointCommon
{
public Task<List<StockTaking>?> GetStockTakings(bool loadRelations);
public Task<List<StockTaking>?> GetStockTakingsByProductId(int productId);
public Task<StockTaking?> AddStockTaking(StockTaking stockTaking);
public Task<StockTaking?> UpdateStockTaking(StockTaking stockTaking);
public Task<List<StockTakingItem>?> GetStockTakingItems();
public Task<StockTakingItem?> GetStockTakingItemsById(int stockTakingItemId);
public Task<List<StockTakingItem>?> GetStockTakingItemsByProductId(int productId);
public Task<List<StockTakingItem>?> GetStockTakingItemsByStockTakingId(int stockTakingId);
public Task<StockTakingItem?> AddStockTakingItem(StockTakingItem stockTakingItem);
public Task<StockTakingItem?> UpdateStockTakingItem(StockTakingItem stockTakingItem);
public Task<List<StockTakingItemPallet>?> GetStockTakingItemPallets();
public Task<List<StockTakingItemPallet>?> GetStockTakingItemPalletsByProductId(int productId);
public Task<StockTakingItemPallet?> AddStockTakingItemPallet(StockTakingItemPallet stockTakingItemPallet);
public Task<StockTakingItemPallet?> UpdateStockTakingItemPallet(StockTakingItemPallet stockTakingItemPallet);
}

View File

@ -1,4 +1,5 @@
using AyCode.Core;
using FruitBank.Common.Services;
using Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Models;
using Nop.Core.Domain.Customers;
@ -7,6 +8,8 @@ namespace FruitBank.Common.Models;
public class LoggedInModel
{
private readonly ISecureCredentialService? _secureCredentialService;
public bool IsLoggedIn => CustomerDto != null;
public bool IsRevisor => IsLoggedIn && CustomerRoles.Any(x => x.SystemName.ToLowerInvariant() == "measuringrevisor");
public bool IsAdministrator => IsLoggedIn && CustomerRoles.Any(x => x.SystemName.ToLowerInvariant() == "administrators");
@ -16,38 +19,123 @@ public class LoggedInModel
public CustomerDto? CustomerDto { get; private set; }
public List<CustomerRole> CustomerRoles { get; private set; } = [];
public List<CustomerDto> MeasuringUsers { get; set; } = [];
public Func<string, string, Task<MgLoginModelResponse?>>? LoginFunc { get; set; }
public Func<int, Task<List<CustomerRole>?>>? GetRolesFunc { get; set; }
public LoggedInModel()
{
}
public LoggedInModel(CustomerDto? customerDto)
public LoggedInModel(ISecureCredentialService secureCredentialService)
{
InitLoggedInCustomer(customerDto);
_secureCredentialService = secureCredentialService;
}
public LoggedInModel(MgLoginModelResponse loginModelResponse) : this(loginModelResponse.CustomerDto)
/// <summary>
/// Tries to login - first checks if already logged in, then checks for stored credentials.
/// Call this on app startup. Only attempts auto-login once per session.
/// </summary>
public async Task<bool> TryAutoLoginAsync()
{
if (IsLoggedIn) return IsLoggedIn;
var credentials = await GetStoredCredentialsAsync();
if (credentials == null) return IsLoggedIn;
await LoginAsync(credentials.Email, credentials.Password, true);
return IsLoggedIn;
}
public void InitLoggedInCustomer(CustomerDto? customerDto)
/// <summary>
/// Performs manual login with the provided credentials.
/// </summary>
public async Task<bool> LoginAsync(string email, string password, bool saveCredentials = true)
{
LogOut();
if (IsLoggedIn || LoginFunc == null) return IsLoggedIn;
var loginResponse = await LoginFunc(email, password);
if (loginResponse is { IsSuccesLogin: true })
{
await SetupLoggedInUser(loginResponse.CustomerDto!);
if (saveCredentials)
{
await SaveCredentialsAsync(email, password);
}
}
return IsLoggedIn;
}
/// <summary>
/// Logs out the user and clears stored credentials.
/// </summary>
public async Task LogOutAsync()
{
await ClearCredentialsAsync();
ClearCustomer();
}
public void SetCustomer(CustomerDto? customerDto)
{
ClearCustomer();
if (customerDto != null) CustomerDto = customerDto;
}
public void InitCustomerRoles(List<CustomerRole> customerRoles)
public void SetCustomerRoles(List<CustomerRole> customerRoles)
{
CustomerRoles.Clear();
CustomerRoles.AddRange(customerRoles);
}
public void LogOut()
public void ClearCustomer()
{
CustomerDto = null;
CustomerRoles.Clear();
//MeasuringUsers.Clear();
}
public void LogOut() => ClearCustomer();
#region Credential Management
public async Task<StoredCredentials?> GetStoredCredentialsAsync()
{
if (_secureCredentialService == null) return null;
return await _secureCredentialService.GetCredentialsAsync();
}
public async Task SaveCredentialsAsync(string email, string password)
{
if (_secureCredentialService == null) return;
await _secureCredentialService.SaveCredentialsAsync(email, password);
}
public async Task ClearCredentialsAsync()
{
if (_secureCredentialService == null) return;
await _secureCredentialService.ClearCredentialsAsync();
}
#endregion
#region Private Methods
private async Task SetupLoggedInUser(CustomerDto customerDto)
{
SetCustomer(customerDto);
if (GetRolesFunc != null)
{
var customerRoles = await GetRolesFunc(customerDto.Id);
if (customerRoles != null)
{
SetCustomerRoles(customerRoles);
}
}
}
#endregion
}

View File

@ -0,0 +1,29 @@
namespace FruitBank.Common.Services;
/// <summary>
/// Service for securely storing and retrieving user credentials.
/// Platform-specific implementations handle the actual secure storage.
/// </summary>
public interface ISecureCredentialService
{
/// <summary>
/// Saves the user credentials securely with a 2-day expiration from now.
/// </summary>
Task SaveCredentialsAsync(string email, string password);
/// <summary>
/// Retrieves the stored credentials if they exist and haven't expired.
/// Returns null if no credentials are stored or if they have expired.
/// </summary>
Task<StoredCredentials?> GetCredentialsAsync();
/// <summary>
/// Clears all stored credentials (used on logout).
/// </summary>
Task ClearCredentialsAsync();
}
/// <summary>
/// Represents stored user credentials.
/// </summary>
public sealed record StoredCredentials(string Email, string Password);

View File

@ -90,6 +90,17 @@ public class SignalRTags : AcSignalRTags
public const int AddGenericAttributeDto = 168;
public const int UpdateGenericAttributeDto = 169;
public const int GetStockTakings = 170;
public const int AddStockTaking = 171;
public const int UpdateStockTaking = 172;
public const int CloseStockTaking = 173;
public const int GetStockTakingItems = 174;
public const int GetStockTakingItemsById = 175;
public const int GetStockTakingItemsByProductId = 176;
public const int GetStockTakingItemsByStockTakingId = 177;
public const int AddOrUpdateMeasuredStockTakingItemPallet = 178;
public const int AuthenticateUser = 195;
public const int RefreshToken = 200;

View File

@ -1,15 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@ -430,16 +430,16 @@ namespace FruitBankHybrid.Shared.Tests
Assert.IsTrue(users.All(x => !x.Email.IsNullOrEmpty() && !x.Deleted));
}
[TestMethod]
[DataRow(CustomerIdAasdDsserverCom)]
public async Task GetCustomerRolesByCustomerIdTest(int customerId)
{
var customerRoles = await _signalRClient.GetCustomerRolesByCustomerId(customerId);
//[TestMethod]
//[DataRow(CustomerIdAasdDsserverCom)]
//public async Task GetCustomerRolesByCustomerIdTest(int customerId)
//{
// var customerRoles = await _signalRClient.GetCustomerRolesByCustomerId(customerId);
Assert.IsNotNull(customerRoles);
Assert.IsTrue(customerRoles.Count > 0);
Assert.IsTrue(customerRoles.Any(cr => cr.SystemName == "Measuring"));
}
// Assert.IsNotNull(customerRoles);
// Assert.IsTrue(customerRoles.Count > 0);
// Assert.IsTrue(customerRoles.Any(cr => cr.SystemName == "Measuring"));
//}
#endregion Customer
@ -461,7 +461,7 @@ namespace FruitBankHybrid.Shared.Tests
//[DataRow(6, false)]
[DataRow(33, true)]
[DataRow(64, false)]
[DataRow(7, false)]
[DataRow(7, true)]
public async Task GetProductDtoByIdTest(int productId, bool isMeasurableExcepted)
{
await GetProductDtoByIdAsync(productId, isMeasurableExcepted);

View File

@ -1,10 +1,12 @@
<Project Sdk="MSTest.Sdk/3.6.4">
<Project Sdk="MSTest.Sdk/4.0.2">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseVSTest>false</UseVSTest>
<!--
Displays error on console in addition to the log file. Note that this feature comes with a performance impact.
For more information, visit https://learn.microsoft.com/dotnet/core/testing/unit-testing-platform-integration-dotnet-test#show-failure-per-test
@ -12,6 +14,11 @@
<TestingPlatformShowTestsFailure>true</TestingPlatformShowTestsFailure>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="bunit" Version="2.2.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
@ -40,7 +47,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="18.0.1" />
</ItemGroup>
<ItemGroup>
@ -48,22 +55,27 @@
</ItemGroup>
<ItemGroup>
<PackageReference Update="Microsoft.Testing.Extensions.TrxReport" Version="2.0.1" />
<PackageReference Update="Microsoft.Testing.Extensions.TrxReport" Version="2.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="MSTest.Analyzers" Version="4.0.1">
<PackageReference Update="MSTest.Analyzers" Version="4.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<PackageReference Update="MSTest.TestAdapter" Version="4.0.1" />
<PackageReference Update="MSTest.TestAdapter" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="MSTest.TestFramework" Version="4.0.1" />
<PackageReference Update="MSTest.TestFramework" Version="4.0.2" />
</ItemGroup>
<!-- Ensure Microsoft.Extensions.ObjectPool v9 is available at test runtime to satisfy SignalR dependency -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,22 @@
using FruitBankHybrid.Shared.Components.Grids.Partners;
using FruitBank.Common.SignalRs;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FruitBankHybrid.Shared.Tests
{
//[TestClass]
//public class GridPartnerBaseTests
//{
// [TestMethod]
// public void Constructor_InitializesMessageTags()
// {
// // Arrange & Act
// var grid = new GridPartnerBase();
// // Assert
// Assert.AreEqual(SignalRTags.GetPartners, grid.GetAllMessageTag);
// Assert.AreEqual(SignalRTags.AddPartner, grid.AddMessageTag);
// Assert.AreEqual(SignalRTags.UpdatePartner, grid.UpdateMessageTag);
// }
//}
}

View File

@ -0,0 +1,20 @@
using Bunit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using FruitBankHybrid.Shared.Components.Grids.Partners;
namespace FruitBankHybrid.Shared.Tests
{
//[TestClass]
//public class GridPartnerRazorTests : Bunit.TestContext
//{
// [TestMethod]
// public void GridPartnerRendersWithoutError()
// {
// // Act
// var cut = RenderComponent<GridPartner>();
// // Assert
// Assert.IsNotNull(cut);
// }
//}
}

View File

@ -0,0 +1,714 @@
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces;
using AyCode.Core.Loggers;
using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions;
using FruitBank.Common;
using FruitBank.Common.Dtos;
using FruitBank.Common.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using FruitBankHybrid.Shared.Tests;
using Microsoft.Extensions.Options;
using Mono.Cecil;
using Newtonsoft.Json;
using Nop.Core.Domain.Orders;
using System.Runtime.Serialization;
namespace FruitBankHybrid.Shared.Tests;
#region Test Models for Hybrid Reference Test
/// <summary>
/// Level 1 - Root entity implementing IId&lt;int&gt; (uses semantic ID: "Company_1")
/// </summary>
public class Company : IId<int>
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// Collection of IId<int> items
public List<Department> Departments { get; set; } = new();
// Non-IId object (uses numeric ID)
public Address HeadquartersAddress { get; set; } = new();
// Array of IId<int> items
public Employee[] BoardMembers { get; set; } = Array.Empty<Employee>();
}
/// <summary>
/// Level 2 - Department implementing IId&lt;int&gt; (uses semantic ID: "Department_1")
/// </summary>
public class Department : IId<int>
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// Back-reference to parent (should use $ref to Company semantic ID)
public Company? ParentCompany { get; set; }
// Collection of IId<int> items
public List<Employee> Employees { get; set; } = new();
// Non-IId object (uses numeric ID)
public Address? OfficeAddress { get; set; }
// Array of IId<Guid> items
public Project[] ActiveProjects { get; set; } = Array.Empty<Project>();
}
/// <summary>
/// Level 3 - Employee implementing IId&lt;int&gt; (uses semantic ID: "Employee_1")
/// </summary>
public class Employee : IId<int>
{
public int Id { get; set; }
public string FullName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
// Back-reference to department (should use $ref to Department semantic ID)
public Department? Department { get; set; }
// Non-IId object (uses numeric ID)
public ContactInfo? Contact { get; set; }
// Collection of IId<Guid> items
public List<Project> AssignedProjects { get; set; } = new();
// Collection of non-IId items (uses numeric IDs)
public List<Skill> Skills { get; set; } = new();
}
/// <summary>
/// Level 4 - Project implementing IId&lt;Guid&gt; (uses semantic ID: "Project_guid")
/// </summary>
public class Project : IId<Guid>
{
public Guid Id { get; set; }
public string ProjectName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
// Back-reference to department (should use $ref to Department semantic ID)
public Department? OwningDepartment { get; set; }
// Collection of IId<int> - circular reference to employees
public List<Employee> TeamMembers { get; set; } = new();
// Array of non-IId items
public Milestone[] Milestones { get; set; } = Array.Empty<Milestone>();
// Collection of IId<long> items
public List<ProjectTask> Tasks { get; set; } = new();
}
/// <summary>
/// Level 5 - ProjectTask implementing IId&lt;long&gt; (uses semantic ID: "ProjectTask_1")
/// Renamed from Task to avoid conflict with System.Threading.Tasks.Task
/// </summary>
public class ProjectTask : IId<long>
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsCompleted { get; set; }
// Back-reference to project (should use $ref to Project semantic ID)
public Project? ParentProject { get; set; }
// Reference to employee (should use $ref to Employee semantic ID)
public Employee? AssignedTo { get; set; }
// Non-IId object
public TaskMetadata? Metadata { get; set; }
}
/// <summary>
/// Non-IId class - Address (uses standard numeric $id)
/// </summary>
public class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
public string PostalCode { get; set; } = string.Empty;
}
/// <summary>
/// Non-IId class - ContactInfo (uses standard numeric $id)
/// </summary>
public class ContactInfo
{
public string Phone { get; set; } = string.Empty;
public string Mobile { get; set; } = string.Empty;
// Shared address reference (should use $ref to numeric ID)
public Address? HomeAddress { get; set; }
}
/// <summary>
/// Non-IId class - Skill (uses standard numeric $id)
/// </summary>
public class Skill
{
public string Name { get; set; } = string.Empty;
public int Level { get; set; }
}
/// <summary>
/// Non-IId class - Milestone (uses standard numeric $id)
/// </summary>
public class Milestone
{
public string Name { get; set; } = string.Empty;
public DateTime DueDate { get; set; }
public bool IsCompleted { get; set; }
}
/// <summary>
/// Non-IId class - TaskMetadata (uses standard numeric $id)
/// </summary>
public class TaskMetadata
{
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public int Priority { get; set; }
public string[] Tags { get; set; } = Array.Empty<string>();
}
#endregion
[TestClass]
public sealed class JsonExtensionTests
{
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public void TestInit()
{
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
{
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
});
}
[TestMethod]
public async Task GetMeasuringUsersTest()
{
var users = await _signalRClient.GetMeasuringUsers();
Assert.IsNotNull(users);
Assert.IsTrue(users.Count != 0);
Assert.IsTrue(users.All(x => !x.Email.IsNullOrEmpty() && !x.Deleted));
}
[TestMethod]
public async Task RealOrderDto_JSON_Merge_Ref_Test()
{
var initialCount = 28;
List<OrderDto>? pendingOrderDtos = (await _signalRClient.GetPendingOrderDtos())?.Take(initialCount).ToList();
Assert.IsNotNull(pendingOrderDtos);
Assert.IsTrue(pendingOrderDtos.Count != 0);
var itemToDuplicate = pendingOrderDtos[0];
var listWithDuplication = pendingOrderDtos.ToList();
listWithDuplication.Add(itemToDuplicate);
var settings = SerializeObjectExtensions.Options;
var dtNow = DateTime.UtcNow;
listWithDuplication[0].PaidDateUtc = dtNow;
var itemWithGenericAttributes = listWithDuplication.FirstOrDefault(x => x.GenericAttributes?.Count > 0);
Assert.IsNotNull(itemWithGenericAttributes, "Nincs olyan OrderDto a listában, amelynek lenne GenericAttributes eleme!");
itemWithGenericAttributes.GenericAttributes[0].CreatedOrUpdatedDateUTC = dtNow;
var json = JsonConvert.SerializeObject(listWithDuplication, settings);
Assert.IsTrue(json.Contains("$id"), "JSON-nak tartalmaznia kell $id tokeneket");
Assert.IsTrue(json.Contains("$ref"), "JSON-nak tartalmaznia kell $ref tokeneket");
pendingOrderDtos.DeepPopulateWithMerge(json, settings);
Assert.AreEqual(initialCount, pendingOrderDtos.Count);
Assert.IsTrue(itemToDuplicate.PaidDateUtc == dtNow);
Assert.IsTrue(itemWithGenericAttributes.GenericAttributes[0].CreatedOrUpdatedDateUTC == dtNow);
}
/// <summary>
/// Comprehensive test for hybrid reference handling:
/// - 5 levels deep hierarchy
/// - IId&lt;int&gt;, IId&lt;Guid&gt;, IId&lt;long&gt; types (semantic IDs)
/// - Non-IId types (numeric IDs)
/// - Collections (List&lt;T&gt;), Arrays, and single object properties
/// - Back-references to parent objects
/// - Shared object references
/// </summary>
[TestMethod]
public void HybridReferenceHandling_DeepHierarchy_Test()
{
// Arrange - Create test data with 5 levels of depth
var sharedAddress = new Address
{
Street = "123 Shared Street",
City = "Budapest",
Country = "Hungary",
PostalCode = "1111"
};
var company = new Company
{
Id = 1,
Name = "Acme Corporation",
HeadquartersAddress = sharedAddress
};
var department1 = new Department
{
Id = 101,
Name = "Engineering",
ParentCompany = company, // Back-reference to Level 1
OfficeAddress = new Address
{
Street = "456 Tech Ave",
City = "Budapest",
Country = "Hungary",
PostalCode = "2222"
}
};
var department2 = new Department
{
Id = 102,
Name = "Marketing",
ParentCompany = company, // Back-reference to Level 1 (same company)
OfficeAddress = sharedAddress // Shared address reference
};
company.Departments.Add(department1);
company.Departments.Add(department2);
var employee1 = new Employee
{
Id = 1001,
FullName = "John Doe",
Email = "john.doe@acme.com",
Department = department1, // Back-reference to Level 2
Contact = new ContactInfo
{
Phone = "+36-1-111-1111",
Mobile = "+36-30-111-1111",
HomeAddress = sharedAddress // Shared address reference
},
Skills = new List<Skill>
{
new() { Name = "C#", Level = 5 },
new() { Name = "Azure", Level = 4 }
}
};
var employee2 = new Employee
{
Id = 1002,
FullName = "Jane Smith",
Email = "jane.smith@acme.com",
Department = department1, // Back-reference to same department
Contact = new ContactInfo
{
Phone = "+36-1-222-2222",
Mobile = "+36-30-222-2222",
HomeAddress = new Address
{
Street = "789 Home Lane",
City = "Debrecen",
Country = "Hungary",
PostalCode = "3333"
}
},
Skills = new List<Skill>
{
new() { Name = "JavaScript", Level = 5 },
new() { Name = "React", Level = 4 }
}
};
var employee3 = new Employee
{
Id = 1003,
FullName = "Bob Wilson",
Email = "bob.wilson@acme.com",
Department = department2, // Different department
Skills = new List<Skill>
{
new() { Name = "Marketing", Level = 5 }
}
};
department1.Employees.Add(employee1);
department1.Employees.Add(employee2);
department2.Employees.Add(employee3);
// Board members array
company.BoardMembers = new[] { employee1, employee3 }; // Shared employee references
var project1 = new Project
{
Id = Guid.NewGuid(),
ProjectName = "Project Alpha",
StartDate = DateTime.UtcNow.AddMonths(-3),
OwningDepartment = department1, // Back-reference to Level 2
TeamMembers = new List<Employee> { employee1, employee2 }, // Shared employee references
Milestones = new[]
{
new Milestone { Name = "Phase 1", DueDate = DateTime.UtcNow.AddMonths(-2), IsCompleted = true },
new Milestone { Name = "Phase 2", DueDate = DateTime.UtcNow.AddMonths(-1), IsCompleted = true },
new Milestone { Name = "Phase 3", DueDate = DateTime.UtcNow.AddMonths(1), IsCompleted = false }
}
};
var project2 = new Project
{
Id = Guid.NewGuid(),
ProjectName = "Project Beta",
StartDate = DateTime.UtcNow.AddMonths(-1),
OwningDepartment = department1, // Same department
TeamMembers = new List<Employee> { employee2 }, // Shared employee reference
Milestones = new[]
{
new Milestone { Name = "Initial", DueDate = DateTime.UtcNow.AddMonths(2), IsCompleted = false }
}
};
department1.ActiveProjects = new[] { project1, project2 };
employee1.AssignedProjects.Add(project1);
employee2.AssignedProjects.Add(project1);
employee2.AssignedProjects.Add(project2);
var task1 = new ProjectTask
{
Id = 10001L,
Title = "Implement Feature X",
Description = "Detailed implementation of Feature X",
IsCompleted = false,
ParentProject = project1, // Back-reference to Level 4
AssignedTo = employee1, // Reference to Level 3
Metadata = new TaskMetadata
{
CreatedAt = DateTime.UtcNow.AddDays(-10),
CreatedBy = "admin",
Priority = 1,
Tags = new[] { "urgent", "feature", "backend" }
}
};
var task2 = new ProjectTask
{
Id = 10002L,
Title = "Write Tests",
Description = "Unit tests for Feature X",
IsCompleted = false,
ParentProject = project1, // Same project
AssignedTo = employee2, // Different employee
Metadata = new TaskMetadata
{
CreatedAt = DateTime.UtcNow.AddDays(-5),
CreatedBy = "admin",
Priority = 2,
Tags = new[] { "testing", "quality" }
}
};
var task3 = new ProjectTask
{
Id = 10003L,
Title = "Review Code",
Description = "Code review for Feature X",
IsCompleted = false,
ParentProject = project1,
AssignedTo = employee1, // Same employee as task1 (circular reference)
Metadata = new TaskMetadata
{
CreatedAt = DateTime.UtcNow.AddDays(-3),
CreatedBy = "lead",
Priority = 1,
Tags = new[] { "review" }
}
};
project1.Tasks.Add(task1);
project1.Tasks.Add(task2);
project1.Tasks.Add(task3);
var settings = SerializeObjectExtensions.Options;
// Act - Serialize
var json = JsonConvert.SerializeObject(company, settings);
// Assert - JSON structure
Assert.IsNotNull(json);
Assert.IsTrue(json.Length > 0, "JSON should not be empty");
// Check for semantic IDs (IId<T> types)
Assert.IsTrue(json.Contains("\"$id\":\"Company_1\""), "Should contain semantic ID for Company");
Assert.IsTrue(json.Contains("\"$id\":\"Department_101\""), "Should contain semantic ID for Department 101");
Assert.IsTrue(json.Contains("\"$id\":\"Department_102\""), "Should contain semantic ID for Department 102");
Assert.IsTrue(json.Contains("\"$id\":\"Employee_1001\""), "Should contain semantic ID for Employee 1001");
Assert.IsTrue(json.Contains("\"$id\":\"Employee_1002\""), "Should contain semantic ID for Employee 1002");
Assert.IsTrue(json.Contains("\"$id\":\"Employee_1003\""), "Should contain semantic ID for Employee 1003");
Assert.IsTrue(json.Contains("\"$id\":\"ProjectTask_10001\""), "Should contain semantic ID for ProjectTask 10001");
// Check for $ref tokens (back-references)
Assert.IsTrue(json.Contains("\"$ref\":\"Company_1\""), "Should contain $ref to Company");
Assert.IsTrue(json.Contains("\"$ref\":\"Department_101\""), "Should contain $ref to Department");
Assert.IsTrue(json.Contains("\"$ref\":\"Employee_1001\""), "Should contain $ref to Employee 1001");
Assert.IsTrue(json.Contains("\"$ref\":\"Employee_1002\""), "Should contain $ref to Employee 1002");
// Check for numeric IDs (non-IId types like Address, ContactInfo, etc.)
// These should have simple numeric $id values
Assert.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(json, @"\$id"":""[0-9]+"""),
"Should contain numeric $id for non-IId types");
// Act - Deserialize
var deserializedCompany = JsonConvert.DeserializeObject<Company>(json, settings);
// Assert - Deserialized structure
Assert.IsNotNull(deserializedCompany);
Assert.AreEqual(1, deserializedCompany.Id);
Assert.AreEqual("Acme Corporation", deserializedCompany.Name);
// Verify departments
Assert.AreEqual(2, deserializedCompany.Departments.Count);
var deserializedDept1 = deserializedCompany.Departments.First(d => d.Id == 101);
var deserializedDept2 = deserializedCompany.Departments.First(d => d.Id == 102);
// Verify back-references to company
Assert.AreSame(deserializedCompany, deserializedDept1.ParentCompany,
"Department1's ParentCompany should be the same instance as deserializedCompany");
Assert.AreSame(deserializedCompany, deserializedDept2.ParentCompany,
"Department2's ParentCompany should be the same instance as deserializedCompany");
// Verify employees
Assert.AreEqual(2, deserializedDept1.Employees.Count);
var deserializedEmp1 = deserializedDept1.Employees.First(e => e.Id == 1001);
var deserializedEmp2 = deserializedDept1.Employees.First(e => e.Id == 1002);
// Verify employee back-references to department
Assert.AreSame(deserializedDept1, deserializedEmp1.Department,
"Employee1's Department should be the same instance as deserializedDept1");
Assert.AreSame(deserializedDept1, deserializedEmp2.Department,
"Employee2's Department should be the same instance as deserializedDept1");
// Verify board members are same instances as department employees
Assert.AreEqual(2, deserializedCompany.BoardMembers.Length);
Assert.AreSame(deserializedEmp1, deserializedCompany.BoardMembers.First(e => e.Id == 1001),
"BoardMember should be the same instance as deserializedEmp1");
// Verify projects
Assert.AreEqual(2, deserializedDept1.ActiveProjects.Length);
var deserializedProject1 = deserializedDept1.ActiveProjects.First(p => p.ProjectName == "Project Alpha");
// Verify project back-reference to department
Assert.AreSame(deserializedDept1, deserializedProject1.OwningDepartment,
"Project1's OwningDepartment should be the same instance as deserializedDept1");
// Verify project team members are same instances
Assert.IsTrue(deserializedProject1.TeamMembers.Any(e => ReferenceEquals(e, deserializedEmp1)),
"Project1's TeamMembers should contain the same instance as deserializedEmp1");
Assert.IsTrue(deserializedProject1.TeamMembers.Any(e => ReferenceEquals(e, deserializedEmp2)),
"Project1's TeamMembers should contain the same instance as deserializedEmp2");
// Verify tasks
Assert.AreEqual(3, deserializedProject1.Tasks.Count);
var deserializedTask1 = deserializedProject1.Tasks.First(t => t.Id == 10001L);
// Verify task back-references
Assert.AreSame(deserializedProject1, deserializedTask1.ParentProject,
"Task1's ParentProject should be the same instance as deserializedProject1");
Assert.AreSame(deserializedEmp1, deserializedTask1.AssignedTo,
"Task1's AssignedTo should be the same instance as deserializedEmp1");
// Verify shared Address instances (non-IId type)
Assert.AreSame(deserializedCompany.HeadquartersAddress, deserializedDept2.OfficeAddress,
"HeadquartersAddress and Dept2's OfficeAddress should be the same instance (shared Address)");
Assert.AreSame(deserializedCompany.HeadquartersAddress, deserializedEmp1.Contact?.HomeAddress,
"HeadquartersAddress and Emp1's HomeAddress should be the same instance (shared Address)");
// Verify milestones (non-IId array)
Assert.AreEqual(3, deserializedProject1.Milestones.Length);
Assert.IsTrue(deserializedProject1.Milestones.Any(m => m.Name == "Phase 1" && m.IsCompleted));
// Verify skills (non-IId collection)
Assert.AreEqual(2, deserializedEmp1.Skills.Count);
Assert.IsTrue(deserializedEmp1.Skills.Any(s => s.Name == "C#" && s.Level == 5));
// Verify task metadata (non-IId object)
Assert.IsNotNull(deserializedTask1.Metadata);
Assert.AreEqual("admin", deserializedTask1.Metadata.CreatedBy);
Assert.AreEqual(1, deserializedTask1.Metadata.Priority);
Assert.AreEqual(3, deserializedTask1.Metadata.Tags.Length);
}
/// <summary>
/// Test for DeepPopulateWithMerge with hybrid references
/// </summary>
[TestMethod]
public void HybridReferenceHandling_DeepPopulateWithMerge_Test()
{
// Arrange - Create initial data
var company = new Company
{
Id = 1,
Name = "Original Company Name",
HeadquartersAddress = new Address { City = "Original City" }
};
var department = new Department
{
Id = 101,
Name = "Original Department",
ParentCompany = company
};
company.Departments.Add(department);
var employee = new Employee
{
Id = 1001,
FullName = "Original Name",
Email = "original@email.com",
Department = department
};
department.Employees.Add(employee);
// Create modified version
var modifiedCompany = new Company
{
Id = 1,
Name = "Modified Company Name",
HeadquartersAddress = new Address { City = "Modified City" }
};
var modifiedDepartment = new Department
{
Id = 101,
Name = "Modified Department",
ParentCompany = modifiedCompany
};
modifiedCompany.Departments.Add(modifiedDepartment);
var modifiedEmployee = new Employee
{
Id = 1001,
FullName = "Modified Name",
Email = "modified@email.com",
Department = modifiedDepartment
};
modifiedDepartment.Employees.Add(modifiedEmployee);
// Add a new employee in the modified version
var newEmployee = new Employee
{
Id = 1002,
FullName = "New Employee",
Email = "new@email.com",
Department = modifiedDepartment
};
modifiedDepartment.Employees.Add(newEmployee);
var settings = SerializeObjectExtensions.Options;
var modifiedJson = JsonConvert.SerializeObject(modifiedCompany, settings);
// Store original references
var originalCompanyRef = company;
var originalDepartmentRef = department;
var originalEmployeeRef = employee;
// Act - Deep populate with merge
company.DeepPopulateWithMerge(modifiedJson, settings);
// Assert - Same instances should be updated, not replaced
Assert.AreSame(originalCompanyRef, company, "Company instance should be the same");
Assert.AreEqual("Modified Company Name", company.Name, "Company name should be updated");
Assert.AreEqual("Modified City", company.HeadquartersAddress.City, "Address should be updated");
Assert.AreEqual(1, company.Departments.Count, "Should still have 1 department");
Assert.AreSame(originalDepartmentRef, company.Departments[0], "Department instance should be the same");
Assert.AreEqual("Modified Department", company.Departments[0].Name, "Department name should be updated");
// The original employee should be updated
var updatedEmployee = company.Departments[0].Employees.FirstOrDefault(e => e.Id == 1001);
Assert.IsNotNull(updatedEmployee);
Assert.AreSame(originalEmployeeRef, updatedEmployee, "Original employee instance should be the same");
Assert.AreEqual("Modified Name", updatedEmployee.FullName, "Employee name should be updated");
Assert.AreEqual("modified@email.com", updatedEmployee.Email, "Employee email should be updated");
// New employee should be added
Assert.AreEqual(2, company.Departments[0].Employees.Count, "Should have 2 employees after merge");
var addedEmployee = company.Departments[0].Employees.FirstOrDefault(e => e.Id == 1002);
Assert.IsNotNull(addedEmployee, "New employee should be added");
Assert.AreEqual("New Employee", addedEmployee.FullName);
}
/// <summary>
/// Test that verifies circular references don't cause infinite loops
/// </summary>
[TestMethod]
public void HybridReferenceHandling_CircularReferences_NoInfiniteLoop_Test()
{
// Arrange - Create circular reference structure
var company = new Company { Id = 1, Name = "Test Company" };
var department = new Department { Id = 101, Name = "Test Dept", ParentCompany = company };
company.Departments.Add(department);
var employee = new Employee { Id = 1001, FullName = "Test Employee", Department = department };
department.Employees.Add(employee);
var project = new Project
{
Id = Guid.NewGuid(),
ProjectName = "Test Project",
OwningDepartment = department,
TeamMembers = new List<Employee> { employee }
};
department.ActiveProjects = new[] { project };
employee.AssignedProjects.Add(project);
var task = new ProjectTask
{
Id = 10001L,
Title = "Test Task",
ParentProject = project,
AssignedTo = employee // Circular: Task -> Employee -> Project -> Task's Project
};
project.Tasks.Add(task);
var settings = SerializeObjectExtensions.Options;
// Act - Should not throw StackOverflowException or timeout
var json = JsonConvert.SerializeObject(company, settings);
var deserialized = JsonConvert.DeserializeObject<Company>(json, settings);
// Assert
Assert.IsNotNull(deserialized);
Assert.AreEqual(1, deserialized.Id);
var deserializedDept = deserialized.Departments[0];
var deserializedEmployee = deserializedDept.Employees[0];
var deserializedProject = deserializedDept.ActiveProjects[0];
var deserializedTask = deserializedProject.Tasks[0];
// Verify circular references are correctly resolved
Assert.AreSame(deserialized, deserializedDept.ParentCompany);
Assert.AreSame(deserializedDept, deserializedEmployee.Department);
Assert.AreSame(deserializedDept, deserializedProject.OwningDepartment);
Assert.AreSame(deserializedProject, deserializedTask.ParentProject);
Assert.AreSame(deserializedEmployee, deserializedTask.AssignedTo);
Assert.AreSame(deserializedEmployee, deserializedProject.TeamMembers[0]);
Assert.AreSame(deserializedProject, deserializedEmployee.AssignedProjects[0]);
}
}

View File

@ -1,10 +1,16 @@
using AyCode.Core.Enums;
using AyCode.Core.Extensions;
using AyCode.Core.Loggers;
using FruitBank.Common;
using FruitBank.Common.Dtos;
using FruitBank.Common.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Newtonsoft.Json;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using System.Runtime.Serialization;
using FruitBank.Common.Entities;
using Nop.Core.Domain.Common;
namespace FruitBankHybrid.Shared.Tests;
@ -28,6 +34,38 @@ public sealed class OrderClientTests
}
[TestMethod]
public async Task GetAllStockTakings()
{
var stockTakings = await _signalRClient.GetStockTakings(true);
Assert.IsNotNull(stockTakings);
Assert.IsTrue(stockTakings.Count != 0);
Assert.IsTrue(stockTakings.All(o => o.StockTakingItems.All(oi => oi.Product != null && oi.Product.Id == oi.ProductId)));
}
[TestMethod]
public async Task GetAllStockTakingItems()
{
var stockTakingItems = await _signalRClient.GetStockTakingItems();
Assert.IsNotNull(stockTakingItems);
Assert.IsTrue(stockTakingItems.Count != 0);
Assert.IsTrue(stockTakingItems.All(oi => oi.StockTaking != null && oi.Product != null && oi.Product.Id == oi.ProductId));
}
[TestMethod]
public async Task GetAllStockTakingItemById()
{
var stockTakingItem = await _signalRClient.GetStockTakingItemsById(100);
Assert.IsNotNull(stockTakingItem);
Assert.IsNotNull(stockTakingItem.Product);
Assert.IsNotNull(stockTakingItem.StockTaking);
}
[TestMethod]
public async Task GetAllOrderDtos()
{
@ -56,10 +94,53 @@ public sealed class OrderClientTests
[TestMethod]
public async Task GetPendingOrderDtos()
{
var pendingOrderDtos = await _signalRClient.GetPendingOrderDtos();
var initialCount = 28;
List<OrderDto>? pendingOrderDtos = (await _signalRClient.GetPendingOrderDtos())?.Take(initialCount).ToList();
Assert.IsNotNull(pendingOrderDtos);
// Másolat létrehozása a frissítendő adatok generálásához
List<OrderDto>? pendingOrderDtos2 = pendingOrderDtos.ToList();
Assert.IsNotNull(pendingOrderDtos2);
Assert.AreEqual(initialCount, pendingOrderDtos2.Count);
// ÚJ BEÁLLÍTÁSOK A RESOLVER-REL ÉS CONTEXT-EL
//var settings = new JsonSerializerSettings
//{
// ContractResolver = new UnifiedMergeContractResolver(),
// ObjectCreationHandling = ObjectCreationHandling.Replace,
// Context = new StreamingContext(StreamingContextStates.All, new Dictionary<object, object>()),
// // Alapvető beállítások
// PreserveReferencesHandling = PreserveReferencesHandling.Objects,
// ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
// NullValueHandling = NullValueHandling.Ignore,
//};
var dtNow = DateTime.UtcNow;
// Eredeti objektum referenciájának eltárolása
var orderDto1 = pendingOrderDtos[0];
// Frissítjük a másolatot
pendingOrderDtos2[0].PaidDateUtc = dtNow;
pendingOrderDtos2[0].GenericAttributes[0].CreatedOrUpdatedDateUTC = dtNow;
pendingOrderDtos2.CopyTo(pendingOrderDtos);
// Szerializálás a Merge Resolverrel
//var json = pendingOrderDtos2.ToJson(settings);
//Assert.IsTrue(json.Contains("$id"));
//Assert.IsTrue(json.Contains("$ref"));
//pendingOrderDtos.DeepPopulateWithMerge(json, settings);
Assert.IsTrue(pendingOrderDtos.Count == pendingOrderDtos2.Count, $"A listák méretének egyeznie kell: {pendingOrderDtos.Count} != {pendingOrderDtos2.Count}");
Assert.IsTrue(orderDto1.PaidDateUtc == dtNow, "A PaidDateUtc mezőnek frissülnie kellett a Merge során.");
Assert.IsTrue(orderDto1.GenericAttributes[0].CreatedOrUpdatedDateUTC == dtNow, "A beágyazott GenericAttribute dátumának frissülnie kellett.");
Assert.IsTrue(pendingOrderDtos.All(o => o.OrderStatus == OrderStatus.Pending));
Assert.IsTrue(pendingOrderDtos.Count != 0);
}
@ -76,7 +157,7 @@ public sealed class OrderClientTests
}
[TestMethod]
[DataRow(new[] {1,2,4,7})]
[DataRow(new[] { 1, 2, 4, 7 })]
public async Task GetOrderDtoByIds(int[] orderIds)
{
var orderDtoList = await _signalRClient.GetAllOrderDtoByIds(orderIds);

View File

@ -0,0 +1,320 @@
using AyCode.Core.Enums;
using AyCode.Core.Loggers;
using AyCode.Utils.Extensions;
using FruitBank.Common;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Interfaces;
using FruitBank.Common.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using System.Diagnostics.CodeAnalysis;
using FruitBank.Common.SignalRs;
using AyCode.Services.SignalRs;
namespace FruitBankHybrid.Shared.Tests;
/// <summary>
/// Teszt a TestSignalREndpoint-hoz.
/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása elõtt!
/// Indítás: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
/// </summary>
[TestClass]
public class SandboxEndpointSimpleTests
{
private static readonly string SandboxUrl = FruitBankConstClient.BaseUrl; //"http://localhost:59579";
private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
// Teszt SignalR Tags (TestSignalRTags-bõl)
private const int PingTag = SignalRTags.PingTag;
private const int EchoTag = SignalRTags.EchoTag;
private const int GetTestItemsTag = 9003;
private FruitBankSignalRClient _signalRClient = null!;
[TestInitialize]
public void TestInit()
{
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
{
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(SandboxEndpointSimpleTests))
});
}
#region HTTP Endpoint Tests
[TestMethod]
public async Task HealthEndpoint_ReturnsSuccess()
{
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync($"{SandboxUrl}/health");
Assert.IsTrue(response.IsSuccessStatusCode, $"Health endpoint returned {response.StatusCode}");
}
[TestMethod]
public async Task RootEndpoint_ReturnsSandboxIsRunning()
{
using var httpClient = new HttpClient();
var response = await httpClient.GetStringAsync(SandboxUrl);
Assert.AreEqual("SANDBOX is running!", response);
}
#endregion
#region SignalR Connection Tests
[TestMethod]
public async Task SignalR_Negotiate_ReturnsSuccess()
{
using var httpClient = new HttpClient();
var response = await httpClient.PostAsync($"{HubUrl}/negotiate?negotiateVersion=1", null);
Assert.IsTrue(response.IsSuccessStatusCode, $"SignalR negotiate returned {response.StatusCode}");
}
[TestMethod]
public async Task SignalR_Connect_Succeeds()
{
var testItems = await _signalRClient.GetAllAsync<List<TestItem>>(GetTestItemsTag);
Assert.IsNotNull(testItems);
}
public class TestItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Value { get; set; }
}
//[TestMethod]
//public async Task SignalR_Connect_Succeeds()
//{
// var connection = new HubConnectionBuilder()
// .WithUrl(HubUrl)
// .Build();
// try
// {
// await connection.StartAsync();
// Assert.AreEqual(HubConnectionState.Connected, connection.State);
// }
// finally
// {
// await connection.StopAsync();
// }
//}
//#endregion
//#region TestSignalREndpoint Tests
//[TestMethod]
//public async Task SignalR_Ping_ReturnsResponse()
//{
// var testMessage = "Hello SignalR!";
// await TestSignalREndpoint(PingTag, testMessage, "Ping", response =>
// {
// Assert.IsNotNull(response, "Response should not be null");
// // Parse JSON response
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenõrizzük, hogy van Message property
// Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) ||
// root.TryGetProperty("message", out messageElement),
// "Response should contain 'Message' property");
// Console.WriteLine($"[Ping] Received message: {messageElement.GetString()}");
// });
//}
//[TestMethod]
//public async Task SignalR_Echo_ReturnsEchoedData()
//{
// var request = new { Id = 42, Name = "TestName" };
// await TestSignalREndpoint(EchoTag, request, "Echo", response =>
// {
// Assert.IsNotNull(response, "Response should not be null");
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenõrizzük az Id-t
// Assert.IsTrue(root.TryGetProperty("Id", out var idElement) ||
// root.TryGetProperty("id", out idElement),
// "Response should contain 'Id' property");
// Assert.AreEqual(42, idElement.GetInt32(), "Id should be 42");
// // Ellenõrizzük a Name-et
// Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) ||
// root.TryGetProperty("name", out nameElement),
// "Response should contain 'Name' property");
// Assert.AreEqual("TestName", nameElement.GetString(), "Name should be 'TestName'");
// Console.WriteLine($"[Echo] Received: Id={idElement.GetInt32()}, Name={nameElement.GetString()}");
// });
//}
//[TestMethod]
//public async Task SignalR_GetTestItems_ReturnsItemList()
//{
// await TestSignalREndpoint(GetTestItemsTag, null, "GetTestItems", response =>
// {
// Assert.IsNotNull(response, "Response should not be null");
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenõrizzük, hogy tömb-e
// Assert.AreEqual(JsonValueKind.Array, root.ValueKind, "Response should be an array");
// Assert.IsTrue(root.GetArrayLength() > 0, "Array should have items");
// Console.WriteLine($"[GetTestItems] Received {root.GetArrayLength()} items");
// // Ellenõrizzük az elsõ elemet
// var firstItem = root[0];
// Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _),
// "Item should have 'Id' property");
// Assert.IsTrue(firstItem.TryGetProperty("Name", out _) || firstItem.TryGetProperty("name", out _),
// "Item should have 'Name' property");
// });
//}
//#endregion
//#region EREDETI BUSINESS ENDPOINT TESZTEK - KIKOMMENTEZVE
//// ===========================================
//// === Az alábbi tesztek az eredeti 3 endpoint-ot tesztelik ===
//// === Visszaállításhoz: töröld a kommenteket és regisztráld az endpoint-okat a Program.cs-ben ===
//// ===========================================
//// [TestMethod]
//// public async Task SignalR_GetMeasuringUsers_ReturnsJson()
//// {
//// await TestSignalREndpoint(GetMeasuringUsersTag, null, "GetMeasuringUsers");
//// }
//// [TestMethod]
//// public async Task SignalR_GetStockQuantityHistoryDtos_ReturnsJson()
//// {
//// await TestSignalREndpoint(GetStockQuantityHistoryDtosTag, null, "GetStockQuantityHistoryDtos");
//// }
//// [TestMethod]
//// public async Task SignalR_GetStockQuantityHistoryDtosByProductId_ReturnsJson()
//// {
//// // ProductId = 10
//// await TestSignalREndpoint(GetStockQuantityHistoryDtosByProductIdTag, 10, "GetStockQuantityHistoryDtosByProductId");
//// }
//// [TestMethod]
//// public async Task SignalR_GetShippingDocumentsByShippingId_ReturnsJson()
//// {
//// // ShippingId = 5
//// await TestSignalREndpoint(GetShippingDocumentsByShippingIdTag, 5, "GetShippingDocumentsByShippingId");
//// }
//// [TestMethod]
//// public async Task SignalR_GetOrderDtoById_ReturnsJson()
//// {
//// // OrderId = 15
//// await TestSignalREndpoint(GetOrderDtoByIdTag, 15, "GetOrderDtoById");
//// }
//// [TestMethod]
//// public async Task SignalR_GetStockTakingItemsById_ReturnsJson()
//// {
//// // StockTakingItemId = 200
//// await TestSignalREndpoint(GetStockTakingItemsByIdTag, 200, "GetStockTakingItemsById");
//// }
//#endregion
//#region Helper Methods
//private async Task TestSignalREndpoint(int tag, object? parameter, string endpointName, Action<string?>? validateResponse = null)
//{
// var connection = new HubConnectionBuilder()
// .WithUrl(HubUrl)
// .Build();
// string? receivedJson = null;
// int receivedTag = -1;
// var responseReceived = new TaskCompletionSource<bool>();
// connection.On<int, byte[]>("ReceiveMessage", (responseTag, data) =>
// {
// receivedTag = responseTag;
// if (data != null && data.Length > 0)
// {
// receivedJson = Encoding.UTF8.GetString(data);
// }
// responseReceived.TrySetResult(true);
// });
// try
// {
// await connection.StartAsync();
// Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}");
// // Készítsük el a request data-t
// // Ha nincs paraméter, null-t küldünk (nem üres byte tömböt!)
// byte[]? requestData = parameter != null
// ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
// : null;
// // A Hub metódus neve: OnReceiveMessage (3 paraméter: messageTag, messageBytes, requestId)
// await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
// var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
// if (completed == responseReceived.Task)
// {
// Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}");
// Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}...");
// // Ellenõrizzük, hogy valid JSON-e (ha van adat)
// if (!string.IsNullOrEmpty(receivedJson))
// {
// try
// {
// using var jsonDoc = JsonDocument.Parse(receivedJson);
// Assert.IsTrue(
// jsonDoc.RootElement.ValueKind == JsonValueKind.Array ||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Object ||
// jsonDoc.RootElement.ValueKind == JsonValueKind.Null,
// $"[{endpointName}] Response is not a valid JSON");
// // Custom validation
// validateResponse?.Invoke(receivedJson);
// }
// catch (JsonException ex)
// {
// Assert.Fail($"[{endpointName}] Invalid JSON response: {ex.Message}");
// }
// }
// }
// else
// {
// Assert.AreEqual(HubConnectionState.Connected, connection.State,
// $"[{endpointName}] Connection was closed - check SANDBOX logs for DI errors");
// }
// }
// catch (Exception ex)
// {
// Assert.Fail($"[{endpointName}] SignalR error: {ex.Message}. Check SANDBOX logs for missing DI registrations.");
// }
// finally
// {
// if (connection.State == HubConnectionState.Connected)
// {
// await connection.StopAsync();
// }
// }
//}
#endregion
}

View File

@ -0,0 +1,17 @@
# Test debugger script for JsonExtensionTests
$projectPath = "H:\Applications\Mango\Source\FruitBankHybridApp"
Set-Location $projectPath
Write-Host "Building test project..."
dotnet build FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj -c Debug
Write-Host "`nRunning JsonExtensionTests..."
# Use --no-build to avoid the MSBuild conflict
dotnet test FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj `
--no-build `
-c Debug `
--filter "ClassName=FruitBankHybrid.Shared.Tests.JsonExtensionTests" `
2>&1 | Tee-Object -FilePath "test_results.txt"
Write-Host "`n=== Test Results ==="
Get-Content "test_results.txt" | Select-String -Pattern "FAILED|PASSED|Error|Assert" | tail -50

View File

@ -14,7 +14,7 @@
ShowFilterRow="IsMasterGrid" ShowGroupPanel="IsMasterGrid"
AutoExpandAllGroupRows="false"
CssClass="@GridCss"
ColumnResizeMode="GridColumnResizeMode.NextColumn" VirtualScrollingEnabled="IsMasterGrid"
ColumnResizeMode="GridColumnResizeMode.NextColumn"
FilterMenuButtonDisplayMode="@(IsMasterGrid ? GridFilterMenuButtonDisplayMode.Never : GridFilterMenuButtonDisplayMode.Always)">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" />
@ -44,19 +44,19 @@
<DxTabPage Text="Rendelés tételek">
<GridDetailOrderItemDto OrderItemDtos="orderDto.OrderItemDtos" IsMasterGrid="false" />
</DxTabPage>
<DxTabPage Text="Mérések">
<DxTabPage Text="Mérések" Visible="@LoggedInModel.IsDeveloper">
@{
var orderItemPalletDtos = orderDto?.OrderItemDtos.SelectMany(oi => oi.OrderItemPallets).ToList() ?? [];
<GridDetailOrderItemPallets OrderItemPallets="orderItemPalletDtos" IsMasterGrid="false"/>
}
</DxTabPage>
<DxTabPage Text="Rendelés jegyzetek">
<DxTabPage Text="Rendelés jegyzetek" Visible="@LoggedInModel.IsDeveloper">
@{
// var orderItemPalletDtos = orderDto?.OrderItemDtos.SelectMany(oi => oi.OrderItemPallets).ToList() ?? [];
// <GridDetailOrderItemPallets OrderItemPallets="orderItemPalletDtos" IsMasterGrid="false" />
}
</DxTabPage>
<DxTabPage Text="Speciális jellemzők">
<DxTabPage Text="Speciális jellemzők" Visible="@LoggedInModel.IsDeveloper">
@{
var genericAttributeDtos = new AcObservableCollection<GenericAttributeDto>(orderDto.GenericAttributes);
<GridGenericAttribute ParentDataItem="@orderDto" GenericAttributes="@genericAttributeDtos" />

View File

@ -62,7 +62,7 @@
<GridDetailOrderItemPallets OrderItemPallets="orderItemDto.OrderItemPallets" />
}
</DxTabPage>
<DxTabPage Text="Speciális jellemzők">
<DxTabPage Text="Speciális jellemzők" Visible="@LoggedInModel.IsDeveloper">
@{
var genericAttributeDtos = new AcObservableCollection<GenericAttributeDto>(orderItemDto.GenericAttributes);
<GridGenericAttribute ParentDataItem="@orderItemDto" GenericAttributes="@genericAttributeDtos" />
@ -111,7 +111,7 @@
{
LoadingPanelVisibility.Visible = true;
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
//using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (ProductDtos == null || !ProductDtos.Any() || forceReload) ProductDtos = await Database.ProductDtoTable.LoadDataAsync(!forceReload);
}

View File

@ -51,16 +51,7 @@
<GridDetailOrderItemDto OrderItemDtos="_currentOrderItemDtos" IsMasterGrid="false" />
}
</DxTabPage>
<DxTabPage Text="Készlet mennyiség változások">
@{
//GetOrderItemDtosFromDbAsync(productId).Forget();
//var orderItemDtos = _orderItemDtos?.Where(oi => oi.ProductId == productId).ToList() ?? [];
var contextIds = new[] { (object)productDto.Id };
<GridStockQuantityHistoryDtoTemplate ContextIds="@(contextIds)" ParentDataItem="@productDto" />
}
</DxTabPage>
<DxTabPage Text="Speciális jellemzők">
<DxTabPage Text="Speciális jellemzők" Visible="@LoggedInModel.IsDeveloper">
@{
var genericAttributeDtos = new AcObservableCollection<GenericAttributeDto>(productDto.GenericAttributes);
<GridGenericAttribute ParentDataItem="@productDto" GenericAttributes="@genericAttributeDtos" />
@ -72,7 +63,7 @@
<ToolbarTemplate>
@if (IsMasterGrid)
{
<FruitBankToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)"/>
<FruitBankToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" />
}
</ToolbarTemplate>
</GridProductDto>
@ -124,7 +115,7 @@
if (!IsMasterGrid) return;
LoadingPanelVisibility.Visible = true;
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
//using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (ProductDtos == null || !ProductDtos.Any() || forceReload) ProductDtos = await Database.ProductDtoTable.LoadDataAsync(!forceReload);
}

View File

@ -17,7 +17,7 @@
<GridShippingItemBase @ref="Grid" ParentDataItem="ParentDataItem" DataSource="ShippingItems" AutoSaveLayoutName="GridShippingItem"
SignalRClient="FruitBankSignalRClient" Logger="_logger"
CssClass="@GridCss" ValidationEnabled="false"
CssClass="@GridCss" ValidationEnabled="false" CustomizeElement="Grid_CustomizeElement"
FocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" Caption="oiId" Width="125" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
@ -75,9 +75,9 @@
<DxGridDataColumn FieldName="GrossWeightOnDocument" Caption="Br.súly(OnDoc)" />
<DxGridDataColumn FieldName="MeasuringCount" Caption="Mérések száma" />
<DxGridDataColumn FieldName="MeasuredQuantity" Caption="Mért mennyiség" ReadOnly="true" />
<DxGridDataColumn FieldName="MeasuredNetWeight" Caption="Mért net.súly(kg)" ReadOnly="true" />
<DxGridDataColumn FieldName="MeasuredGrossWeight" Caption="Mért br.súly(kg)" ReadOnly="true" />
<DxGridDataColumn FieldName="MeasuredQuantity" Name="MeasuredQuantity" Caption="Mért mennyiség" ReadOnly="true" />
<DxGridDataColumn FieldName="MeasuredNetWeight" Name="MeasuredNetWeight" Caption="Mért net.súly(kg)" ReadOnly="true" />
<DxGridDataColumn FieldName="MeasuredGrossWeight" Name="MeasuredGrossWeight" Caption="Mért br.súly(kg)" ReadOnly="true" />
<DxGridDataColumn FieldName="IsMeasurable" ReadOnly="true" />
<DxGridDataColumn FieldName="IsMeasured" ReadOnly="true" />
@ -159,7 +159,7 @@
public async Task ReloadDataFromDb(bool forceReload = false)
{
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
//using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (ProductDtos == null || !ProductDtos.Any() || forceReload) ProductDtos = await Database.ProductDtoTable.LoadDataAsync(!forceReload);
}
@ -190,6 +190,55 @@
if (forceReload) Grid.Reload();
}
void Grid_CustomizeElement(GridCustomizeElementEventArgs e)
{
if (e.ElementType != GridElementType.DataCell) return;
if (e.Column.Name != nameof(ShippingItem.MeasuredNetWeight) &&
e.Column.Name != nameof(ShippingItem.MeasuredGrossWeight) &&
e.Column.Name != nameof(ShippingItem.MeasuredQuantity)) return;
var isMeasured = (bool)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.IsMeasured));
if (!isMeasured) return;
switch (e.Column.Name)
{
case nameof(ShippingItem.MeasuredNetWeight) or nameof(ShippingItem.MeasuredGrossWeight):
{
var isMeasurable = (bool)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.IsMeasurable));
if (!isMeasurable) return;
var valueOnDocument = 0d;
var measuredValue = 0d;
if (e.Column.Name == nameof(ShippingItem.MeasuredGrossWeight))
{
valueOnDocument = (double)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.GrossWeightOnDocument));
measuredValue = (double)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.MeasuredGrossWeight));
}
else
{
valueOnDocument = (double)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.NetWeightOnDocument));
measuredValue = (double)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.MeasuredNetWeight));
}
if (valueOnDocument > 0 && valueOnDocument > measuredValue) e.CssClass = "text-danger";
//else if (valueOnDocument <= measuredValue) e.CssClass = "text-success";
break;
}
case nameof(ShippingItem.MeasuredQuantity):
{
var quantityOnDocument = (int)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.QuantityOnDocument));
var measuredQuantity = (int)e.Grid.GetRowValue(e.VisibleIndex, nameof(ShippingItem.MeasuredQuantity));
if (quantityOnDocument > 0 && quantityOnDocument > measuredQuantity) e.CssClass = "text-danger";
//else if (quantityOnDocument <= measuredQuantity) e.CssClass = "text-success";
break;
}
}
}
async Task Grid_FocusedRowChanged(GridFocusedRowChangedEventArgs args)
{
if ((args.Grid.IsEditing() || args.Grid.IsEditingNewRow()) && (args.DataItem as IId<int>).Id > 0)

View File

@ -17,7 +17,18 @@
<Columns>
<DxGridDataColumn FieldName="Id" Width="125" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
<DxGridDataColumn FieldName="ProductId" Width="125" ReadOnly="true" Visible="false" />
<DxGridDataColumn FieldName="ProductId" ReadOnly="true" Visible="@IsMasterGrid">
<EditSettings>
<DxComboBoxSettings Data="ProductDtos"
ValueFieldName="Id"
TextFieldName="Name"
DropDownBodyCssClass="dd-body-class"
ListRenderMode="ListRenderMode.Entire"
SearchMode="ListSearchMode.AutoSearch"
SearchFilterCondition="ListSearchFilterCondition.Contains"
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto" />
</EditSettings>
</DxGridDataColumn>
<DxGridDataColumn FieldName="QuantityAdjustment" Width="135" Caption="Adj. Quantity" ReadOnly="true" />
<DxGridDataColumn FieldName="StockQuantity" Width="135" ReadOnly="true" />
@ -77,7 +88,7 @@
{
if (!IsMasterGrid) return;
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
//using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (ProductDtos == null || !ProductDtos.Any() || forceReload) ProductDtos = await Database.ProductDtoTable.LoadDataAsync(!forceReload);
}

View File

@ -54,6 +54,11 @@ public class GridShippingItemBase : FruitBankGridBase<ShippingItem>, IGrid
await base.OnInitializedAsync();
}
//protected override Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
//{
// return base.OnCustomizeEditModel(e);
//}
protected override void OnParametersSet()
{
base.OnParametersSet();

View File

@ -0,0 +1,121 @@
@using System.Collections.ObjectModel
@using AyCode.Core.Helpers
@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.Components.Toolbars
@using FruitBankHybrid.Shared.Databases
@using FruitBankHybrid.Shared.Services.Loggers
@using FruitBankHybrid.Shared.Services.SignalRs
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
<GridStockTakingItemBase @ref="Grid" AutoSaveLayoutName="GridStockTakingItem" SignalRClient="FruitBankSignalRClient" Logger="_logger"
CssClass="@GridCss" ValidationEnabled="false" FocusedRowChanged="Grid_FocusedRowChanged">
<Columns>
<DxGridDataColumn FieldName="Id" SortIndex="0" SortOrder="GridColumnSortOrder.Descending" ReadOnly="true" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.StockTakingId)" TextAlignment="GridTextAlignment.Left" Caption="Leltár időpontja">
<CellDisplayTemplate>
<span>@(((StockTakingItem)context.DataItem)?.StockTaking?.StartDateTime.ToString("g") ?? "")</span>
</CellDisplayTemplate>
</DxGridDataColumn>
<DxGridDataColumn FieldName="Product.Name" />
<DxGridDataColumn FieldName="OriginalStockQuantity" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.InProcessOrdersQuantity)" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.TotalOriginalQuantity)" />
<DxGridDataColumn FieldName="MeasuredStockQuantity" />
<DxGridDataColumn FieldName="OriginalNetWeight" />
<DxGridDataColumn FieldName="MeasuredNetWeight" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.QuantityDiff)" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.NetWeightDiff)" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.IsMeasurable)" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.IsMeasured)" />
<DxGridDataColumn FieldName="@nameof(StockTakingItem.IsInvalid)" />
<DxGridDataColumn FieldName="Created" ReadOnly="true" Visible="false" DisplayFormat="g" />
<DxGridDataColumn FieldName="Modified" ReadOnly="true" DisplayFormat="g" />
<DxGridCommandColumn Visible="!IsMasterGrid" Width="120"></DxGridCommandColumn>
</Columns>
<ToolbarTemplate>
@if (IsMasterGrid)
{
<FruitBankToolbarTemplate Grid="Grid" OnReloadDataClick="() => ReloadDataFromDb(true)" />
}
</ToolbarTemplate>
</GridStockTakingItemBase>
@code {
//[Inject] public required ObjectLock ObjectLock { get; set; }
[Inject] public required DatabaseClient Database { get; set; }
[Parameter] public bool IsMasterGrid { get; set; } = false;
[Parameter] public AcObservableCollection<Partner>? Partners { get; set; }
[Parameter] public AcObservableCollection<Shipping>? Shippings { get; set; }
const string ExportFileName = "ExportResult";
string GridSearchText = "";
bool EditItemsEnabled { get; set; }
int FocusedRowVisibleIndex { get; set; }
public GridStockTakingItemBase Grid { get; set; }
string GridCss => !IsMasterGrid ? "hide-toolbar" : string.Empty;
private int _activeTabIndex;
private LoggerClient<GridStockTakingItem> _logger;
protected override async Task OnInitializedAsync()
{
_logger = new LoggerClient<GridStockTakingItem>(LogWriters.ToArray());
await ReloadDataFromDb(false);
}
private async Task ReloadDataFromDb(bool forceReload = false)
{
if (!IsMasterGrid) return;
if (Grid == null) return;
//using (await ObjectLock.GetSemaphore<StockTakingItem>().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;
}
// void Grid_CustomGroup(GridCustomGroupEventArgs e)
// {
// if (e.FieldName != "StockTaking.StartDateTime") return;
// e.SameGroup = ((StockTakingItem)e.DataItem1).StockTakingId == ((StockTakingItem)e.DataItem2).StockTakingId;
// e.Handled = true;
// }
// void Grid_CustomizeGroupValueDisplayText(GridCustomizeGroupValueDisplayTextEventArgs e)
// {
// return;
// if (e.FieldName != "StockTaking.StartDateTime") return;
// var startDate = (DateTime)e.Value;
// e.DisplayText = startDate.ToString("g");
// }
}

View File

@ -0,0 +1,67 @@
using AyCode.Core.Interfaces;
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.StockTakingItems;
public class GridStockTakingItemBase: FruitBankGridBase<StockTakingItem>, IGrid
{
private bool _isFirstInitializeParameterCore;
private bool _isFirstInitializeParameters;
public GridStockTakingItemBase() : base()
{
GetAllMessageTag = SignalRTags.GetStockTakingItems;
//AddMessageTag = SignalRTags.AddPartner;
//UpdateMessageTag = SignalRTags.UpdatePartner;
//RemoveMessageTag = SignalRTags.;
}
protected override async Task OnInitializedAsync()
{
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;
}
}
}

View File

@ -8,6 +8,7 @@
@using FruitBankHybrid.Shared.Extensions
@using FruitBankHybrid.Shared.Services
@using FruitBankHybrid.Shared.Services.SignalRs
@typeparam TPalletItem where TPalletItem : class, IMeasuringItemPalletBase
<DxFormLayout Context="ctxFromLayoutPallet" Data="@PalletItem" CaptionPosition="CaptionPosition.Vertical" CssClass="w-100 measuring-form-layout"
@ -181,6 +182,8 @@
StateHasChanged(); //Az Audit button miatt kell a StateHasChanged(), most már van RevisorId és emiatt disabled lesz...
PalletItem.SetParentPropToNull();
var responseShippingItemPallet = await FruitBankSignalRClient.PostDataAsync(AddOrUpdateSignalRTag!.Value, PalletItem);
if (responseShippingItemPallet == null)
{

View File

@ -0,0 +1,222 @@
@using AyCode.Core.Extensions
@using AyCode.Utils.Extensions
@using DevExpress.Blazor
@using FruitBank.Common.Dtos
@using FruitBank.Common.Entities
@using FruitBank.Common.Helpers
@using FruitBank.Common.Models
@using FruitBank.Common.SignalRs
@using FruitBankHybrid.Shared.Databases
@using FruitBankHybrid.Shared.Extensions
@using FruitBankHybrid.Shared.Services
@using FruitBankHybrid.Shared.Services.SignalRs
@using Mango.Nop.Core.Entities
<DxFormLayout CaptionPosition="CaptionPosition.Vertical" CssClass="w-100">
<DxFormLayoutItem Caption="Termék:" ColSpanMd="2">
@* CaptionCssClass="@(SelectedProductDto?.IsMeasured == true ? "text-success" : "")"> *@
<DxComboBox Data="@_stockTakings"
TextFieldName="@nameof(StockTaking.StartDateTime)"
CssClass="cw-480"
DropDownBodyCssClass="dd-body-class"
Context="ctxProduct"
InputId="cbProduct"
Value="@SelectedStockTaking"
ValueChanged="@(async (StockTaking stockTaking) => await StockTakingComboValueChanged(stockTaking))">
</DxComboBox>
</DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="4">
<DxComboBox Data="@_stockTakingItems"
@bind-Value="@SelectedStockTakingItem"
TextFieldName="@nameof(StockTakingItem.DisplayText)"
CssClass="cw-480"
DropDownBodyCssClass="dd-body-class"
Context="ctxProduct2"
InputId="cbProduct2">
</DxComboBox>
</DxFormLayoutItem>
@* TextFieldName="StockTakingItem.Product.Name" *@
<DxFormLayoutItem ColSpanMd="1">
<DxButton Text="Új" Enabled="@(_stockTakings.All(x => x.IsClosed))" Click="() => NewStockTakingClick()"></DxButton>
</DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="1">
<DxButton Text="Módosít" Enabled="@(SelectedStockTaking?.IsClosed ?? false)" Click="() => UpdateStockTakingClick()"></DxButton>
</DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="1">
<DxButton Text="Lezárás" Enabled="@(SelectedStockTaking?.IsReadyForClose() ?? false)" Click="() => StockTakingCloseClick()"></DxButton>
</DxFormLayoutItem>
</DxFormLayout>
<div style="margin-top: 50px;">
@if (SelectedStockTakingItem is { ProductId: > 0 })
{
<h3 style="margin-bottom: 30px;" class="@(SelectedStockTakingItem.IsMeasured && SelectedStockTakingItem.StockTakingItemPallets!.All(x => x.IsMeasuredAndValid(SelectedStockTakingItem.IsMeasurable)) ? "text-success" : "")">
#@(SelectedStockTakingItem.ProductId). @(SelectedStockTakingItem.Product!.Name)
</h3>
<div>
@{
var a = $"Várható rekesz: {SelectedStockTakingItem.TotalOriginalQuantity} ({SelectedStockTakingItem.OriginalStockQuantity} + {SelectedStockTakingItem.InProcessOrdersQuantity}), Várható net.súly: {SelectedStockTakingItem.OriginalNetWeight} kg.";
<span>@a</span>
}
</div>
<DxFormLayout Data="@SelectedStockTakingItem" CaptionPosition="CaptionPosition.Vertical" CssClass="w-100">
<DxFormLayoutItem Context="ctxShippingItemFromLayoutItem" ColSpanMd="12">
@for (var index = 0; index < (SelectedStockTakingItem?.StockTakingItemPallets?.Count ?? 0); index++)
{
var localI = index + 1;
var currentShippingItemPallet = SelectedStockTakingItem!.StockTakingItemPallets![index];
<PalletItemComponent IsMeasurable="@SelectedStockTakingItem!.IsMeasurable"
MeasuringIndex="@localI"
PalletItem="@currentShippingItemPallet"
ProductId="@SelectedStockTakingItem.Product!.Id"
AddOrUpdateSignalRTag="SignalRTags.AddOrUpdateMeasuredStockTakingItemPallet"
OnPalletItemSaved="pallet => OnStockTakingItemPalletSaved(pallet)"
OnPalletItemValueChanged="pallet => OnStockTakingItemPalletValueChanged(pallet, SelectedStockTakingItem)">
</PalletItemComponent>
}
</DxFormLayoutItem>
@* <DxFormLayoutItem Context="vfdfgfd" ColSpanMd="12" BeginRow="true">
<DxFormLayout CssClass="w-100">
<DxFormLayoutItem ColSpanMd="1" BeginRow="false"><strong>TOTAL:</strong></DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="2" BeginRow="false" Visible="@(SelectedStockTakingItem.IsMeasurable)" />
<DxFormLayoutItem ColSpanMd="2" BeginRow="false" Visible="@(SelectedStockTakingItem.IsMeasurable)" />
<DxFormLayoutItem ColSpanMd="2" BeginRow="false"><strong>Rekesz: @(SelectedStockTakingItem.MeasuredStockQuantity) db</strong></DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="2" BeginRow="false" Visible="@(SelectedStockTakingItem.IsMeasurable)"><strong>Br: @(SelectedStockTakingItem.MeasuredGrossWeight) kg</strong></DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="1" BeginRow="false" Visible="@(SelectedStockTakingItem.IsMeasurable)"><strong>Net: @(SelectedStockTakingItem.MeasuredNetWeight) kg</strong></DxFormLayoutItem>
<DxFormLayoutItem ColSpanMd="1" BeginRow="false" />
</DxFormLayout>
</DxFormLayoutItem>
*@
</DxFormLayout>
}
</div>
@code
{
[Inject] public required DatabaseClient Database { get; set; }
[Inject] public required LoggedInModel LoggedInModel { get; set; }
[Inject] public required IDialogService DialogService { get; set; } = null!;
[Inject] public required FruitBankSignalRClient FruitBankSignalRClient { get; set; }
List<StockTaking> _stockTakings { get; set; } = [];
List<StockTakingItem> _stockTakingItems { get; set; } = [];
List<StockTakingItemPallet> _stockTakingItemPallets { get; set; } = [];
StockTaking? SelectedStockTaking { get; set; }
StockTakingItem? SelectedStockTakingItem { get; set; }
protected override async Task OnInitializedAsync()
{
await ReloadDataFromDb(false);
}
public async Task ReloadDataFromDb(bool forceReload)
{
LoadingPanelVisibility.Visible = true;
_stockTakings = await FruitBankSignalRClient.GetStockTakings(false) ?? [];
await StockTakingComboValueChanged(_stockTakings.FirstOrDefault());
LoadingPanelVisibility.Visible = false;
}
private async Task NewStockTakingClick()
{
var stockTaking = new StockTaking();
stockTaking.StartDateTime = DateTime.Now;
stockTaking.Creator = LoggedInModel.CustomerDto!.Id;
var resultStockTakings = await FruitBankSignalRClient.AddStockTaking(stockTaking);
if (resultStockTakings == null) return;
_stockTakings.UpdateCollection(resultStockTakings, false);
await StockTakingComboValueChanged(_stockTakings.FirstOrDefault(x => x.Id == stockTaking.Id));
}
private async Task UpdateStockTakingClick()
{
// var resultStockTaking = await FruitBankSignalRClient.AddStockTaking(stockTaking);
// if (resultStockTaking == null) return;
// _stockTakings.Add(resultStockTaking);
StateHasChanged();
}
private async Task StockTakingCloseClick()
{
// var resultStockTaking = await FruitBankSignalRClient.AddStockTaking(stockTaking);
// if (resultStockTaking == null) return;
// _stockTakings.Add(resultStockTaking);
StateHasChanged();
}
private async Task StockTakingComboValueChanged(StockTaking? newValue)
{
SelectedStockTaking = newValue;
SelectedStockTaking?.StockTakingItems = await FruitBankSignalRClient.GetStockTakingItemsByStockTakingId(SelectedStockTaking.Id);
PrepareStockTakingItems(SelectedStockTaking);
SelectedStockTakingItem = _stockTakingItems.FirstOrDefault();
StateHasChanged();
}
private void PrepareStockTakingItems(StockTaking? stockTaking)
{
_stockTakingItems = stockTaking?.StockTakingItems?
.OrderByDescending(x => x.IsInvalid)
.ThenByDescending(x => x.IsRequiredForMeasuring)
.ThenBy(x => x.Product?.Name)
.ToList() ?? [];
foreach (var stockTakingItem in _stockTakingItems)
{
stockTakingItem.StockTakingItemPallets ??= [];
stockTakingItem.StockTaking = stockTaking;
if (!stockTakingItem.IsInvalid && stockTakingItem.StockTakingItemPallets.Count == 0)
{
stockTakingItem.StockTakingItemPallets.Add(MeasurementService.CreateNewStockTakingItemPallet(stockTakingItem, LoggedInModel.CustomerDto));
}
else
{
foreach (var stockTakingItemPallet in stockTakingItem.StockTakingItemPallets)
stockTakingItemPallet.StockTakingItem = stockTakingItem;
}
}
}
private Task OnStockTakingItemPalletValueChanged(StockTakingItemPallet stockTakingItemPallet, StockTakingItem stockTakingItem)
{
// MeasuringValuesHelper.SetShippingItemTotalMeasuringValues(stockTakingItem);
// BtnSaveEnabled = stockTakingItem.IsValidMeasuringValues() && stockTakingItemPallet.IsValidMeasuringValues(stockTakingItem.IsMeasurable);
StateHasChanged();
return Task.CompletedTask;
}
private async Task OnStockTakingItemPalletSaved(StockTakingItemPallet? responseStockTakingItemPallet)
{
if (responseStockTakingItemPallet != null)
{
responseStockTakingItemPallet.StockTakingItem = SelectedStockTakingItem;
SelectedStockTakingItem!.MeasuredStockQuantity = responseStockTakingItemPallet.TrayQuantity;
if (SelectedStockTakingItem.IsMeasurable) SelectedStockTakingItem.MeasuredNetWeight = responseStockTakingItemPallet.NetWeight;
SelectedStockTakingItem.StockTakingItemPallets!.UpdateCollection(responseStockTakingItemPallet, false);
SelectedStockTakingItem.IsMeasured = SelectedStockTakingItem.StockTakingItemPallets!.All(sip => sip.IsMeasuredAndValid(SelectedStockTakingItem.IsMeasurable));
// MeasuringValuesHelper.SetShippingItemTotalMeasuringValues(SelectedShippingItem);
}
else await DialogService.ShowMessageBoxAsync("Hiba", "Adatok mentése sikertelen volt, ellenőrizze a mérés adatait!", MessageBoxRenderStyle.Danger);
await InvokeAsync(StateHasChanged);
}
}

View File

@ -1,4 +1,5 @@
@using AyCode.Core.Loggers;
@using AyCode.Blazor.Components.Components.Grids
@using AyCode.Core.Loggers;
@using AyCode.Core.Extensions
@using AyCode.Core.Helpers
@using AyCode.Utils.Extensions
@ -10,6 +11,8 @@
@using FruitBankHybrid.Shared.Services.Loggers;
@using FruitBankHybrid.Shared.Services.SignalRs
@implements IDisposable
@inject IEnumerable<IAcLogWriterClientBase> LogWriters
@inject FruitBankSignalRClient FruitBankSignalRClient
@inject LoggedInModel LoggedInModel;
@ -30,23 +33,6 @@
<DxToolbarItem Text="Reload data" BeginGroup="true" Click="ReloadData_Click" Enabled="@BtnReloadDataEnabled" />
<DxToolbarItem BeginGroup="true">
</DxToolbarItem>
@* <DxToolbarItem BeginGroup="true">
<Template Context="toolbar_item_context">
<DxSearchBox @bind-Text="GridSearchText"
BindValueMode="BindValueMode.OnInput"
ClearButtonDisplayMode="DataEditorClearButtonDisplayMode.Auto"
aria-label="Search" />
</Template>
</DxToolbarItem>*@
@* @if (DxToolbarItems != null)
{
foreach (var toolBarItem in DxToolbarItems)
{
@toolBarItem
}
//@DxToolbarItem
} *@
@ToolbarItemsExtended
@ -60,20 +46,67 @@
public ToolbarBase Toolbar { get; set; }
const string ExportFileName = "ExportResult";
public bool BtnReloadDataEnabled = true;
private bool _isReloadInProgress;
private bool _isGridSyncing;
private IMgGridBase? _mgGrid;
/// <summary>
/// Reload button is enabled only when no sync operation is in progress
/// </summary>
public bool BtnReloadDataEnabled => !_isReloadInProgress && !_isGridSyncing;
public bool EditItemsEnabled { get; set; } = true;
private LoggerClient<GridShippingItemTemplate> _logger;
protected override async Task OnInitializedAsync()
protected override void OnInitialized()
{
_logger = new LoggerClient<GridShippingItemTemplate>(LogWriters.ToArray());
}
protected override void OnParametersSet()
{
// Subscribe to grid syncing state changes if Grid implements IMgGridBase
if (Grid is IMgGridBase mgGrid && !ReferenceEquals(_mgGrid, mgGrid))
{
// Unsubscribe from previous grid
if (_mgGrid != null)
{
_mgGrid.OnSyncingStateChanged -= OnGridSyncingStateChanged;
}
_mgGrid = mgGrid;
_mgGrid.OnSyncingStateChanged += OnGridSyncingStateChanged;
// Get initial syncing state
_isGridSyncing = _mgGrid.IsSyncing;
}
}
private void OnGridSyncingStateChanged(bool isSyncing)
{
_isGridSyncing = isSyncing;
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
if (_mgGrid != null)
{
_mgGrid.OnSyncingStateChanged -= OnGridSyncingStateChanged;
}
}
async Task ReloadData_Click(ToolbarItemClickEventArgs e)
{
BtnReloadDataEnabled = false;
_isReloadInProgress = true;
try
{
await OnReloadDataClick.InvokeAsync();
BtnReloadDataEnabled = true;
}
finally
{
_isReloadInProgress = false;
}
}
async Task NewItem_Click()

View File

@ -58,12 +58,13 @@ public class ShippingItemTable : SignalRDataSourceList<ShippingItemTableItem>
public class ProductDtoTable(FruitBankSignalRClient fruitBankSignalRClient) : AcObservableCollection<ProductDtoTableItem>
{
private readonly SemaphoreSlim _semaphoreSlim = new(1);
//private readonly SemaphoreSlim _semaphoreSlim = new(1);
public async Task<ProductDtoTable> LoadDataAsync(bool onlyIfEmpty = true)
{
if (onlyIfEmpty && Count > 0) return this;
using (await _semaphoreSlim.UseWaitAsync())
//using (await _semaphoreSlim.UseWaitAsync())
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
//Előfordulhat, h egy másik szálban már megtörtént a refresh... - J.
if (onlyIfEmpty && Count > 0) return this;
@ -83,13 +84,14 @@ public class ProductDtoTable(FruitBankSignalRClient fruitBankSignalRClient) : Ac
}
public class OrderDtoTable(FruitBankSignalRClient fruitBankSignalRClient) : AcObservableCollection<OrderDtoTableItem>
{
private readonly SemaphoreSlim _semaphoreSlim = new(1);
//private readonly SemaphoreSlim _semaphoreSlim = new(1);
public async Task<OrderDtoTable> LoadDataAsync(bool onlyIfEmpty = true)
{
if (onlyIfEmpty && Count > 0) return this;
using (await _semaphoreSlim.UseWaitAsync())
//using (await _semaphoreSlim.UseWaitAsync())
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (Count > 0) return this;

View File

@ -1,9 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
</PropertyGroup>
<ItemGroup>
@ -14,9 +17,9 @@
<PackageReference Include="DevExpress.Blazor" Version="25.1.3" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,5 @@
@using FruitBank.Common.Models
@using FruitBankHybrid.Shared.Pages
@inherits LayoutComponentBase
<div class="page">
@ -29,7 +30,12 @@
ShowCloseButton="true">
</DxToastProvider>
<CascadingValue Value="RefreshMainLayoutEventCallback">
@* @Body *@
@if (LoggedInModel.IsLoggedIn || IsOnLoginPage)
{
@Body
}
</CascadingValue>
</article>
</main>

View File

@ -12,7 +12,6 @@ using FruitBankHybrid.Shared.Pages;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Mango.Nop.Core.Loggers;
using MessagePack.Resolvers;
using Microsoft.AspNetCore.Components;
namespace FruitBankHybrid.Shared.Layout;
@ -29,6 +28,8 @@ public partial class MainLayout : LayoutComponentBase
private NavMenu _navMenu = null!;
private LoggerClient _logger = null!;
private bool IsOnLoginPage => NavManager.Uri.Equals(NavManager.ToAbsoluteUri("/Login").ToString(), StringComparison.OrdinalIgnoreCase);
// Toast fields
private DxToast orderNotificationToast;
private string toastTitle = "Értesítő!";
@ -39,14 +40,29 @@ public partial class MainLayout : LayoutComponentBase
protected override void OnInitialized()
{
_logger = new LoggerClient<MainLayout>(LogWriters.ToArray());
_logger.Info("OnInitializedAsync");
var loginUri = NavManager.ToAbsoluteUri("/Login").ToString();
FruitBankSignalRClient.OnMessageReceived += SignalRClientOnMessageReceived;
_logger.Info("OnInitialized");
if (!LoggedInModel.IsLoggedIn && NavManager.Uri != loginUri)
// Setup login delegates
LoggedInModel.LoginFunc = FruitBankSignalRClient.LoginMeasuringUser;
LoggedInModel.GetRolesFunc = FruitBankSignalRClient.GetCustomerRolesByCustomerId;
FruitBankSignalRClient.OnMessageReceived += SignalRClientOnMessageReceived;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
await LoggedInModel.TryAutoLoginAsync();
if (!LoggedInModel.IsLoggedIn && !IsOnLoginPage)
{
NavManager.NavigateTo("/Login");
}
else if (LoggedInModel.IsLoggedIn)
{
StateHasChanged(); // Refresh UI after successful auto-login
}
}
private async Task SignalRClientOnMessageReceived(int messageTag, string? jsonMessage)
@ -85,9 +101,9 @@ public partial class MainLayout : LayoutComponentBase
});
}
private void OnLogoutClick()
private async void OnLogoutClick()
{
LoggedInModel.LogOut();
await LoggedInModel.LogOutAsync();
RefreshMainLayout();
NavManager.NavigateTo("/Login");
}
@ -97,4 +113,9 @@ public partial class MainLayout : LayoutComponentBase
_navMenu.RefreshNavMenu();
StateHasChanged();
}
public void Dispose()
{
FruitBankSignalRClient.OnMessageReceived -= SignalRClientOnMessageReceived;
}
}

View File

@ -37,6 +37,15 @@
</NavLink>
</div>
@if (LoggedInModel.IsDeveloper)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="StockTaking">
<span class="icon counter-icon" aria-hidden="true"></span> Leltározás
</NavLink>
</div>
}
@if (LoggedInModel.IsAdministrator)
{
<div class="nav-item px-3">
@ -44,9 +53,7 @@
<span class="icon counter-icon" aria-hidden="true"></span> Rendelések - Adminisztrátor
</NavLink>
</div>
}
@if (LoggedInModel.IsAdministrator)
{
<div class="nav-item px-3">
<NavLink class="nav-link" href="ShippingsAdmin">
<span class="icon counter-icon" aria-hidden="true"></span> Szállítmányok - Adminisztrátor

View File

@ -10,9 +10,4 @@ public partial class Home : ComponentBase
private string Factor => FormFactor.GetFormFactor();
private string Platform => FormFactor.GetPlatform();
protected override void OnInitialized()
{
if (!LoggedInModel.IsLoggedIn) NavManager.NavigateTo("/Login");
}
}

View File

@ -20,11 +20,10 @@ public partial class Login : ComponentBase
[Inject] public required NavigationManager NavManager{ get; set; }
private ILogger _logger = null!;
//private List<CustomerDto> Users { get; set; }
private CustomerDto? SelectedUser { get; set; }
private string PasswordValue { get; set; } = string.Empty;
private MgLoginModelResponse? LoginModelResponse { get; set; }
private string _rolesText = string.Empty;
[CascadingParameter]
public EventCallback UpdateStyle { get; set; }
@ -35,6 +34,18 @@ public partial class Login : ComponentBase
_logger.Info("OnInitializedAsync");
if (!LoggedInModel.IsLoggedIn)
{
await LoadMeasuringUsersAsync();
}
else
{
_rolesText = string.Join("; ", LoggedInModel.CustomerRoles.Select(x => x.Name));
}
await base.OnInitializedAsync();
}
private async Task LoadMeasuringUsersAsync()
{
using (await ObjectLock.GetSemaphore<CustomerDto>().UseWaitAsync())
{
@ -45,46 +56,42 @@ public partial class Login : ComponentBase
}
}
}
else _rolesText = string.Join("; ", LoggedInModel.CustomerRoles.Select(x => x.Name));
await base.OnInitializedAsync();
}
private string _rolesText = string.Empty;
private async Task OnLoginClick()
{
if (LoggedInModel.IsLoggedIn) return;
_rolesText = string.Empty;
if (!ValidateLoginInput()) return;
// Use the simplified LoginAsync from LoggedInModel
if(await LoggedInModel.LoginAsync(SelectedUser!.Email, PasswordValue))
{
_rolesText = string.Join("; ", LoggedInModel.CustomerRoles.Select(x => x.Name));
}
await UpdateStyle.InvokeAsync();
if (LoggedInModel.IsLoggedIn)
{
NavManager.NavigateTo("/");
}
StateHasChanged();
}
private bool ValidateLoginInput()
{
if (SelectedUser == null || PasswordValue.IsNullOrWhiteSpace())
{
LoginModelResponse = new MgLoginModelResponse
{
ErrorMessage = "Válasszon felhsználót és adja meg a jelszavát!"
};
return;
return false;
}
LoginModelResponse = await FruitBankSignalRClient.LoginMeasuringUser(SelectedUser.Email, PasswordValue);
if (LoginModelResponse is { IsSuccesLogin: true })
{
LoggedInModel.InitLoggedInCustomer(LoginModelResponse.CustomerDto);
var customerRoles = await FruitBankSignalRClient.GetCustomerRolesByCustomerId(LoginModelResponse.CustomerDto!.Id);
if (customerRoles != null)
{
LoggedInModel.InitCustomerRoles(customerRoles);
_rolesText = string.Join("; ", LoggedInModel.CustomerRoles.Select(x => x.Name));
}
}
await UpdateStyle.InvokeAsync();
if (LoggedInModel.IsLoggedIn) NavManager.NavigateTo("/");
StateHasChanged();
return true;
}
protected async Task OnPasswordKeyDown(KeyboardEventArgs e)
@ -94,7 +101,6 @@ public partial class Login : ComponentBase
private string GetImageFileName(CustomerDto employee)
{
//return StaticAssetUtils.GetEmployeeImagePath(employee.Id);
return string.Empty;
}
}

View File

@ -39,8 +39,6 @@ namespace FruitBankHybrid.Shared.Pages
protected override async Task OnInitializedAsync()
{
if (!LoggedInModel.IsLoggedIn) NavManager.NavigateTo("/Login");
LoadingPanelVisible = true;
_logger = new LoggerClient<MeasuringIn>(LogWriters.ToArray());
@ -156,6 +154,8 @@ namespace FruitBankHybrid.Shared.Pages
{
if (responseShippingItemPallet != null)
{
responseShippingItemPallet.ShippingItem = SelectedShippingItem;
SelectedShippingItem!.ShippingItemPallets!.UpdateCollection(responseShippingItemPallet, false);
SelectedShippingItem.IsMeasured = SelectedShippingItem!.ShippingItemPallets!.All(sip => sip.IsMeasuredAndValid(SelectedShippingItem.IsMeasurable));
@ -197,8 +197,14 @@ namespace FruitBankHybrid.Shared.Pages
shippingItem.ShippingItemPallets ??= new List<ShippingItemPallet>(shippingItem.MeasuringCount);
for (var i = shippingItem.ShippingItemPallets.Count; i < shippingItem.MeasuringCount; i++)
for (var i = 0; i < shippingItem.MeasuringCount; i++)
{
if (i < shippingItem.ShippingItemPallets.Count)
{
shippingItem.ShippingItemPallets[i].ShippingItem = shippingItem;
continue;
}
shippingItem.ShippingItemPallets.Add(MeasurementService.CreateNewShippingItemPallet(shippingItem, LoggedInModel.CustomerDto));
}
}

View File

@ -41,8 +41,6 @@ namespace FruitBankHybrid.Shared.Pages
protected override async Task OnInitializedAsync()
{
if (!LoggedInModel.IsLoggedIn) NavManager.NavigateTo("/Login");
LoadingPanelVisible = true;
_logger = new LoggerClient<MeasuringOut>(LogWriters.ToArray());
_logger.Info("OnInitializedAsync");
@ -93,10 +91,14 @@ namespace FruitBankHybrid.Shared.Pages
if (SelectedDate != orderDto.DateOfReceipt.Value.Date) return;
var selectedOrderId = SelectedOrder?.Id;
SelectedDayOrders.UpdateCollection(orderDto, false);
//Elég lenne ez is, csak a CopyTo a Collection - ökben lévő elemeket hozzáfűzi és duplikálva lesznek... -J.
if (SelectedOrder?.Id == orderDto.Id) orderDto.CopyTo(SelectedOrder);
else SelectedDayOrders.UpdateCollection(orderDto, false);
if (selectedOrderId.GetValueOrDefault(-1) == orderDto.Id) SelectedOrder = orderDto;
//var selectedOrderId = SelectedOrder?.Id;
//SelectedDayOrders.UpdateCollection(orderDto, false);
//if (selectedOrderId.GetValueOrDefault(-1) == orderDto.Id) SelectedOrder = orderDto;
}
await InvokeAsync(StateHasChanged);
@ -108,7 +110,11 @@ namespace FruitBankHybrid.Shared.Pages
lock (_lock)
{
var localOrderDto = SelectedDayOrders.FirstOrDefault(o => o.OrderItemDtos.Any(oi => oi.Id == orderItemDto.Id));
var localOrderItemDto = localOrderDto?.OrderItemDtos.First(x => x.Id == orderItemDto.Id)!;
var localOrderItemDto = localOrderDto?.OrderItemDtos.FirstOrDefault(x => x.Id == orderItemDto.Id);
if (localOrderItemDto == null) return;
//orderItemDto.OrderDto = localOrderDto!;
localOrderItemDto.Quantity = orderItemDto.Quantity;
localOrderItemDto.GenericAttributes.UpdateBaseEntityCollection(orderItemDto.GenericAttributes, false);
@ -127,6 +133,7 @@ namespace FruitBankHybrid.Shared.Pages
orderItemDto = orderItemDtos?.FirstOrDefault(oi => oi.Id == orderItemPallet.OrderItemId);
if (orderItemDto == null) return;
orderItemPallet.OrderItemDto = orderItemDto;
var orderItemPalletsCount = orderItemDto.OrderItemPallets.Count;
if (orderItemDto.OrderItemPallets[orderItemPalletsCount - 1].Id == 0) orderItemDto.OrderItemPallets.Insert(orderItemPalletsCount - 1, orderItemPallet);
@ -161,7 +168,7 @@ namespace FruitBankHybrid.Shared.Pages
SelectedDayOrders = orders.Where(order => MeasurementService.DaysEqual(order.DateOfReceiptOrCreated, dateTime)).OrderBy(x => x.DateOfReceipt).ToList();
foreach (var orderDto in SelectedDayOrders) PrepareOrderDto(orderDto);
foreach (var orderDto in SelectedDayOrders) PrepareOrderItemDtos(orderDto);
SelectedOrder = LoggedInModel.IsRevisor
? SelectedDayOrders.FirstOrDefault(o => o is { IsComplete: false, IsMeasured: true })
@ -233,16 +240,18 @@ namespace FruitBankHybrid.Shared.Pages
return Task.CompletedTask;
}
private Task OnPalletItemAuditedClick(OrderItemPallet? orderItemPallet, OrderItemDto selectedOrderItemDto)
private async Task OnPalletItemAuditedClick(OrderItemPallet? orderItemPallet, OrderItemDto selectedOrderItemDto)
{
StateHasChanged();
return Task.CompletedTask;
if (orderItemPallet == null) return;
await OnOrderItemPalletSaved(orderItemPallet, selectedOrderItemDto);
}
private async Task OnOrderItemPalletSaved(OrderItemPallet? orderItemPallet, OrderItemDto selectedOrderItemDto)
{
if (orderItemPallet != null)
{
orderItemPallet.OrderItemDto = selectedOrderItemDto;
selectedOrderItemDto.OrderItemPallets.UpdateCollection(orderItemPallet, false);
//MeasuringValuesHelper.SetShippingItemTotalMeasuringValues(SelectedShippingItem);
}
@ -291,7 +300,7 @@ namespace FruitBankHybrid.Shared.Pages
var responseOrderDto = await FruitBankSignalRClient.StartMeasuring(SelectedOrder.Id, LoggedInModel.CustomerDto!.Id);
if (responseOrderDto != null)
{
PrepareOrderDto(responseOrderDto);
PrepareOrderItemDtos(responseOrderDto);
//SelectedOrder.GenericAttributes.UpdateBaseEntityCollection(responseOrderDto.GenericAttributes, false);
SelectedDayOrders.UpdateCollection(responseOrderDto, false);
@ -311,7 +320,7 @@ namespace FruitBankHybrid.Shared.Pages
var responseOrderDto = await FruitBankSignalRClient.SetOrderStatusToComplete(SelectedOrder.Id, LoggedInModel.CustomerDto!.Id);
if (responseOrderDto != null)
{
PrepareOrderDto(responseOrderDto);
PrepareOrderItemDtos(responseOrderDto);
SelectedDayOrders.UpdateCollection(responseOrderDto, false);
SelectedOrder = responseOrderDto;
@ -325,11 +334,18 @@ namespace FruitBankHybrid.Shared.Pages
}
}
private void PrepareOrderDto(OrderDto orderDto)
private void PrepareOrderItemDtos(OrderDto orderDto)
{
foreach (var orderItemDto in orderDto.OrderItemDtos.Where(orderItem => orderItem.OrderItemPallets.Count == 0))
foreach (var orderItemDto in orderDto.OrderItemDtos)
{
orderItemDto.OrderItemPallets.Add(MeasurementService.CreateNewOrderItemPallet(orderItemDto, LoggedInModel.CustomerDto));
orderItemDto.OrderDto = orderDto;
if (orderItemDto.OrderItemPallets.Count == 0) orderItemDto.OrderItemPallets.Add(MeasurementService.CreateNewOrderItemPallet(orderItemDto, LoggedInModel.CustomerDto));
else
{
foreach (var orderItemPallet in orderItemDto.OrderItemPallets)
orderItemPallet.OrderItemDto = orderItemDto;
}
}
}

View File

@ -49,7 +49,7 @@ public partial class OrdersAdmin : ComponentBase
private async Task ReloadDataFromDb(bool forceReload = false)
{
LoadingPanelVisibility.Visible = true;
using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
//using (await ObjectLock.GetSemaphore<ProductDto>().UseWaitAsync())
{
if (ProductDtos == null || !ProductDtos.Any() || forceReload) ProductDtos = await Database.ProductDtoTable.LoadDataAsync(!forceReload);
}

View File

@ -0,0 +1,36 @@
@page "/StockTaking"
@using FruitBankHybrid.Shared.Components.Grids.Products
@using FruitBankHybrid.Shared.Components.Grids.StockTakingItems
@using FruitBankHybrid.Shared.Components.StockTakings
@using FruitBankHybrid.Shared.Databases
<h3>Leltározás</h3>
<DxDialogProvider />
<div style="margin-top: 30px;">
<DxLoadingPanel @bind-Visible="LoadingPanelVisibility.Visible"
IsContentBlocked="true"
ApplyBackgroundShading="true"
IndicatorAreaVisible="false"
Text="Adatok szinkronizálása folyamatban...">
<DxTabs RenderMode="TabsRenderMode.OnDemand">
<DxTabPage Text="Leltározás">
<StockTakingTemplate></StockTakingTemplate>
</DxTabPage>
<DxTabPage Text="Leltár előzmények">
@{
<GridStockTakingItem IsMasterGrid="true"></GridStockTakingItem>
}
</DxTabPage>
<DxTabPage Text="Készlet mennyiség változások">
@{
<GridStockQuantityHistoryDtoTemplate></GridStockQuantityHistoryDtoTemplate>
}
</DxTabPage>
</DxTabs>
</DxLoadingPanel>
</div>

View File

@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FruitBankHybrid.Shared.Pages
{
public partial class StockTaking : ComponentBase
{
}
}

View File

@ -0,0 +1,3 @@
// This file is kept for backward compatibility.
// The interface has been moved to FruitBank.Common.Services.ISecureCredentialService
global using FruitBank.Common.Services;

View File

@ -65,7 +65,7 @@ public class MeasurementService(IEnumerable<IAcLogWriterClientBase> logWriters)
public static ShippingItemPallet CreateNewShippingItemPallet(ShippingItem shippingItem, CustomerDto? customerDto)
{
var shippingItemPallet = CreatePalletItem<ShippingItemPallet>(shippingItem.Id, shippingItem.ProductDto?.Tare, shippingItem.IsMeasurable, customerDto);
var shippingItemPallet = CreatePalletItemBase<ShippingItemPallet>(shippingItem.Id, shippingItem.ProductDto?.Tare, shippingItem.IsMeasurable, customerDto);
shippingItemPallet.ShippingItem = shippingItem;
shippingItemPallet.PalletWeight = shippingItem.IsMeasurable ? shippingItem.Pallet?.Weight ?? 0 : 0;
@ -75,13 +75,24 @@ public class MeasurementService(IEnumerable<IAcLogWriterClientBase> logWriters)
public static OrderItemPallet CreateNewOrderItemPallet(OrderItemDto orderItemDto, CustomerDto? customerDto)
{
var orderItemPallet = CreatePalletItem<OrderItemPallet>(orderItemDto.Id, orderItemDto.ProductDto?.Tare, orderItemDto.IsMeasurable, customerDto);
var orderItemPallet = CreatePalletItemBase<OrderItemPallet>(orderItemDto.Id, orderItemDto.ProductDto?.Tare, orderItemDto.IsMeasurable, customerDto);
orderItemPallet.OrderItemDto = orderItemDto;
return orderItemPallet;
}
private static TPalletItem CreatePalletItem<TPalletItem>(int foreignKey, double? tare, bool isMeasurable, CustomerDto? customerDto) where TPalletItem : MeasuringItemPalletBase
public static StockTakingItemPallet CreateNewStockTakingItemPallet(StockTakingItem stockTakingItem, CustomerDto? customerDto)
{
var stockTakingItemPallet = CreatePalletItemBase<StockTakingItemPallet>(stockTakingItem.Id, stockTakingItem.Product?.Tare, stockTakingItem.IsMeasurable, customerDto);
stockTakingItemPallet.GrossWeight = stockTakingItem.IsMeasurable ? -1 : 0;
stockTakingItemPallet.TrayQuantity = -1;
stockTakingItemPallet.StockTakingItem = stockTakingItem;
return stockTakingItemPallet;
}
private static TPalletItem CreatePalletItemBase<TPalletItem>(int foreignKey, double? tare, bool isMeasurable, CustomerDto? customerDto) where TPalletItem : MeasuringItemPalletBase
{
var palletItem = Activator.CreateInstance<TPalletItem>();

View File

@ -19,10 +19,11 @@ using Microsoft.AspNetCore.SignalR.Client;
using Nop.Core.Domain.Customers;
using System.Collections.ObjectModel;
using System.ServiceModel.Channels;
using Mango.Nop.Core.Entities;
namespace FruitBankHybrid.Shared.Services.SignalRs
{
public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient
public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient
{
public FruitBankSignalRClient( /*IServiceProvider serviceProvider, */ IEnumerable<IAcLogWriterClientBase> logWriters) : base($"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}", new LoggerClient(nameof(FruitBankSignalRClient), logWriters.ToArray()))
{
@ -263,5 +264,73 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
=> GetAllAsync<List<StockQuantityHistoryDto>>(SignalRTags.GetStockQuantityHistoryDtosByProductId, [productId]);
#endregion Orders
public async Task<List<GenericAttributeDto>?> GetGenericAttributeDtosByEntityIdAndKeyGroup(int productId, string keyGroup, int storeId)
{
throw new NotImplementedException();
}
public async Task<GenericAttributeDto?> AddGenericAttributeDto(GenericAttributeDto genericAttributeDto)
{
throw new NotImplementedException();
}
public async Task<GenericAttributeDto?> UpdateGenericAttributeDto(GenericAttributeDto genericAttributeDto)
{
throw new NotImplementedException();
}
public Task<List<StockTaking>?> GetStockTakings(bool loadRelations) => GetAllAsync<List<StockTaking>>(SignalRTags.GetStockTakings, [loadRelations]);
public async Task<List<StockTaking>?> GetStockTakingsByProductId(int productId)
{
throw new NotImplementedException();
}
public Task<StockTaking?> AddStockTaking(StockTaking stockTaking) => PostDataAsync(SignalRTags.AddStockTaking, stockTaking);
public Task<StockTaking?> UpdateStockTaking(StockTaking stockTaking) => PostDataAsync(SignalRTags.UpdateStockTaking, stockTaking);
public Task<List<StockTakingItem>?> GetStockTakingItems() => GetAllAsync<List<StockTakingItem>>(SignalRTags.GetStockTakingItems);
public Task<StockTakingItem?> GetStockTakingItemsById(int stockTakingItemId)
=> GetByIdAsync<StockTakingItem>(SignalRTags.GetStockTakingItemsById, [stockTakingItemId]);
public Task<List<StockTakingItem>?> GetStockTakingItemsByProductId(int productId)
=> GetAllAsync<List<StockTakingItem>>(SignalRTags.GetStockTakingItemsByProductId, [productId]);
public Task<List<StockTakingItem>?> GetStockTakingItemsByStockTakingId(int stockTakingId)
=> GetAllAsync<List<StockTakingItem>>(SignalRTags.GetStockTakingItemsByStockTakingId, [stockTakingId]);
public async Task<StockTakingItem?> AddStockTakingItem(StockTakingItem stockTakingItem)
{
throw new NotImplementedException();
}
public async Task<StockTakingItem?> UpdateStockTakingItem(StockTakingItem stockTakingItem)
{
throw new NotImplementedException();
}
public async Task<List<StockTakingItemPallet>?> GetStockTakingItemPallets()
{
throw new NotImplementedException();
}
public async Task<List<StockTakingItemPallet>?> GetStockTakingItemPalletsByProductId(int productId)
{
throw new NotImplementedException();
}
public async Task<StockTakingItemPallet?> AddStockTakingItemPallet(StockTakingItemPallet stockTakingItemPallet)
{
throw new NotImplementedException();
}
public async Task<StockTakingItemPallet?> UpdateStockTakingItemPallet(StockTakingItemPallet stockTakingItemPallet)
{
throw new NotImplementedException();
}
}
}

View File

@ -7,4 +7,10 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using FruitBankHybrid
@using FruitBankHybrid.Shared
@using FruitBankHybrid.Shared.Layout
@using FruitBankHybrid.Shared.Components
@using FruitBankHybrid.Shared.Components.Grids.Products
@using DevExpress.Blazor

View File

@ -1,19 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DevExpress.Blazor" Version="25.1.*" />
<PackageReference Include="DevExpress.Blazor" Version="25.1.3" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<StaticWebAssetFingerprintPattern Include="JS" Pattern="*.js" Expression="#[.{fingerprint}]!" />
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
using AyCode.Core.Loggers;
using FruitBank.Common.Loggers;
using FruitBank.Common.Models;
using FruitBank.Common.Services;
using FruitBankHybrid.Shared.Databases;
using FruitBankHybrid.Shared.Services;
using FruitBankHybrid.Shared.Services.SignalRs;
@ -15,12 +16,14 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres
// Add device-specific services used by the FruitBankHybrid.Shared project
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, WebSecureCredentialService>();
//#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
//#endif
builder.Services.AddSingleton<LoggedInModel>();
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();

View File

@ -0,0 +1,122 @@
using System.Text;
using System.Text.Json;
using FruitBank.Common.Services;
using Microsoft.JSInterop;
namespace FruitBankHybrid.Web.Client.Services;
/// <summary>
/// WebAssembly implementation of ISecureCredentialService using obfuscated localStorage.
/// Note: WebAssembly has limited cryptography support, so we use Base64 + XOR obfuscation.
/// This prevents casual inspection but is not cryptographically secure.
/// For true security, consider server-side token storage or authentication cookies.
/// </summary>
public sealed class WebSecureCredentialService : ISecureCredentialService
{
private const string CredentialsKey = "FruitBank_UserCredentials";
private static readonly TimeSpan ExpirationDuration = TimeSpan.FromDays(2);
private static readonly byte[] ObfuscationKey = "FruitBank_Secure_v1_2025"u8.ToArray();
private readonly IJSRuntime _jsRuntime;
public WebSecureCredentialService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task SaveCredentialsAsync(string email, string password)
{
var data = new SecureCredentialData
{
Email = email,
Password = password,
ExpiresAtUtc = DateTime.UtcNow.Add(ExpirationDuration)
};
var json = JsonSerializer.Serialize(data);
var obfuscated = Obfuscate(json);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CredentialsKey, obfuscated);
}
public async Task<StoredCredentials?> GetCredentialsAsync()
{
try
{
var obfuscated = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CredentialsKey);
if (string.IsNullOrEmpty(obfuscated))
return null;
var json = Deobfuscate(obfuscated);
if (string.IsNullOrEmpty(json))
{
await ClearCredentialsAsync();
return null;
}
var data = JsonSerializer.Deserialize<SecureCredentialData>(json);
if (data == null)
return null;
// Check expiration
if (DateTime.UtcNow > data.ExpiresAtUtc)
{
await ClearCredentialsAsync();
return null;
}
return new StoredCredentials(data.Email, data.Password);
}
catch
{
// If any error occurs (corrupted data, etc.), clear and return null
await ClearCredentialsAsync();
return null;
}
}
public async Task ClearCredentialsAsync()
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", CredentialsKey);
}
private static string Obfuscate(string plainText)
{
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var obfuscatedBytes = new byte[plainBytes.Length];
for (int i = 0; i < plainBytes.Length; i++)
{
obfuscatedBytes[i] = (byte)(plainBytes[i] ^ ObfuscationKey[i % ObfuscationKey.Length]);
}
return Convert.ToBase64String(obfuscatedBytes);
}
private static string? Deobfuscate(string obfuscated)
{
try
{
var obfuscatedBytes = Convert.FromBase64String(obfuscated);
var plainBytes = new byte[obfuscatedBytes.Length];
for (int i = 0; i < obfuscatedBytes.Length; i++)
{
plainBytes[i] = (byte)(obfuscatedBytes[i] ^ ObfuscationKey[i % ObfuscationKey.Length]);
}
return Encoding.UTF8.GetString(plainBytes);
}
catch
{
return null;
}
}
private sealed class SecureCredentialData
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public DateTime ExpiresAtUtc { get; set; }
}
}

View File

@ -6,6 +6,7 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using FruitBankHybrid.Shared
@using FruitBankHybrid.Web.Client

View File

@ -8,18 +8,23 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
@* <ResourcePreloader /> *@
@DxResourceManager.RegisterScripts()
@DxResourceManager.RegisterTheme(Themes.Fluent)
<link href=@AppendVersion("_content/FruitBankHybrid.Shared/bootstrap/bootstrap.min.css") rel="stylesheet" />
<link href=@AppendVersion("_content/FruitBankHybrid.Shared/app.css") rel="stylesheet" />
<link href=@AppendVersion("FruitBankHybrid.Web.styles.css") rel="stylesheet" />
<ImportMap />
<link href=@AppendVersion("_content/FruitBankHybrid.Shared/favicon.png") rel="icon" type="image/png" />
<HeadOutlet @rendermode="InteractiveWebAssembly" />
</head>
<body class="dxbl-theme-fluent">
<Routes @rendermode="InteractiveWebAssembly" />
<script src="_framework/blazor.web.js"></script>
<Routes @rendermode="InteractiveWebAssembly" />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

View File

@ -6,7 +6,9 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using FruitBankHybrid.Shared
@using FruitBankHybrid.Shared.Layout
@using FruitBankHybrid.Web
@using FruitBankHybrid.Web.Client
@using FruitBankHybrid.Web.Components

View File

@ -1,10 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!--<PublishTrimmed>true</PublishTrimmed>-->
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
@ -17,9 +21,9 @@
<PackageReference Include="DevExpress.Blazor" Version="25.1.3" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.10" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.11" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
using AyCode.Core.Loggers;
using FruitBank.Common;
using FruitBank.Common.Models;
using FruitBank.Common.Services;
using FruitBank.Common.Server.Services.Loggers;
using FruitBank.Common.Server.Services.SignalRs;
using FruitBankHybrid.Shared.Databases;
@ -11,15 +12,17 @@ using FruitBankHybrid.Web.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents().AddInteractiveWebAssemblyComponents();
builder.Services.AddRazorComponents().AddInteractiveWebAssemblyComponents();
builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium);
builder.Services.AddMvc();
builder.Services.AddSignalR(options => options.MaximumReceiveMessageSize = 256 * 1024);
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, ServerSecureCredentialService>();
builder.Services.AddSingleton<IAcLogWriterBase, ConsoleLogWriter>();
builder.Services.AddSingleton<LoggedInModel>();
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();

View File

@ -0,0 +1,30 @@
using FruitBank.Common.Services;
namespace FruitBankHybrid.Web.Services;
/// <summary>
/// Server-side implementation of ISecureCredentialService.
/// This is a no-op implementation used during prerendering - actual credential storage
/// is handled by the client-side WebSecureCredentialService after WebAssembly loads.
/// </summary>
public sealed class ServerSecureCredentialService : ISecureCredentialService
{
public Task SaveCredentialsAsync(string email, string password)
{
// No-op on server side - credentials are stored client-side
return Task.CompletedTask;
}
public Task<StoredCredentials?> GetCredentialsAsync()
{
// Always return null on server side - auto-login happens client-side after WASM loads
return Task.FromResult<StoredCredentials?>(null);
}
public Task ClearCredentialsAsync()
{
// No-op on server side
return Task.CompletedTask;
}
}

View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36414.22
# Visual Studio Version 18
VisualStudioVersion = 18.0.11222.15 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruitBankHybrid", "FruitBankHybrid\FruitBankHybrid.csproj", "{85ADEDE3-C271-47DF-B273-2EDB32792CEF}"
EndProject

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.26100.0</TargetFrameworks>
<TargetFrameworks>net10.0-android;net10.0-ios</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.26100.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>FruitBankHybrid</RootNamespace>
@ -22,14 +22,18 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<!--<WindowsPackageType>None</WindowsPackageType>-->
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">29.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">33.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.19041.0</TargetPlatformMinVersion>
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
<AppxPackageSigningEnabled>False</AppxPackageSigningEnabled>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
@ -53,14 +57,14 @@
<UseInterpreter>true</UseInterpreter>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net9.0-android|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net10.0-android|AnyCPU'">
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidUseAapt2>True</AndroidUseAapt2>
<AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net9.0-android|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0-android|AnyCPU'">
<AndroidCreatePackagePerAbi>False</AndroidCreatePackagePerAbi>
<AndroidUseAapt2>True</AndroidUseAapt2>
<AndroidPackageFormat>apk</AndroidPackageFormat>
@ -89,9 +93,10 @@
<!--<PackageReference Include="DevExpress.Maui.Controls" Version="25.1.3" />
<PackageReference Include="DevExpress.Maui.Editors" Version="25.1.3" />
<PackageReference Include="DevExpress.Maui.CollectionView" Version="25.1.3" />-->
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.120" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.120" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.10" />
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.11" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>

View File

@ -1,6 +1,7 @@
using AyCode.Core.Loggers;
using FruitBank.Common.Loggers;
using FruitBank.Common.Models;
using FruitBank.Common.Services;
using FruitBankHybrid.Services;
using FruitBankHybrid.Services.Loggers;
using FruitBankHybrid.Shared.Databases;
@ -28,12 +29,14 @@ namespace FruitBankHybrid
// Add device-specific services used by the FruitBankHybrid.Shared project
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, MauiSecureCredentialService>();
#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
#endif
builder.Services.AddSingleton<LoggedInModel>();
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();

View File

@ -0,0 +1,68 @@
using System.Text.Json;
using FruitBank.Common.Services;
namespace FruitBankHybrid.Services;
/// <summary>
/// MAUI implementation of ISecureCredentialService using SecureStorage.
/// </summary>
public sealed class MauiSecureCredentialService : ISecureCredentialService
{
private const string CredentialsKey = "FruitBank_UserCredentials";
private static readonly TimeSpan ExpirationDuration = TimeSpan.FromDays(2);
public async Task SaveCredentialsAsync(string email, string password)
{
var data = new SecureCredentialData
{
Email = email,
Password = password,
ExpiresAtUtc = DateTime.UtcNow.Add(ExpirationDuration)
};
var json = JsonSerializer.Serialize(data);
await SecureStorage.Default.SetAsync(CredentialsKey, json);
}
public async Task<StoredCredentials?> GetCredentialsAsync()
{
try
{
var json = await SecureStorage.Default.GetAsync(CredentialsKey);
if (string.IsNullOrEmpty(json))
return null;
var data = JsonSerializer.Deserialize<SecureCredentialData>(json);
if (data == null)
return null;
// Check expiration
if (DateTime.UtcNow > data.ExpiresAtUtc)
{
await ClearCredentialsAsync();
return null;
}
return new StoredCredentials(data.Email, data.Password);
}
catch
{
// If any error occurs (corrupted data, etc.), clear and return null
await ClearCredentialsAsync();
return null;
}
}
public Task ClearCredentialsAsync()
{
SecureStorage.Default.Remove(CredentialsKey);
return Task.CompletedTask;
}
private sealed class SecureCredentialData
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public DateTime ExpiresAtUtc { get; set; }
}
}