Compare commits
43 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
db9f7aa12f | |
|
|
ea643d0855 | |
|
|
431ed8c037 | |
|
|
352b3b2d21 | |
|
|
5b0b080b5a | |
|
|
40d6567f29 | |
|
|
9f8dbd29fc | |
|
|
ab51b948ae | |
|
|
25522f7c27 | |
|
|
aecd54ffdd | |
|
|
ac7b4d58df | |
|
|
711c3c8ec0 | |
|
|
33d84a8257 | |
|
|
10f325cc26 | |
|
|
26b40cf7a1 | |
|
|
27ac2d1843 | |
|
|
8f48838ded | |
|
|
322f38f1fa | |
|
|
dbccbf487d | |
|
|
3f49945bfb | |
|
|
5e4bb4c8e0 | |
|
|
90419001ab | |
|
|
e393718c20 | |
|
|
8c90a6ba51 | |
|
|
e2c49940c6 | |
|
|
45195b9cdf | |
|
|
ecd7275cee | |
|
|
c5e841f207 | |
|
|
d1c254d5d1 | |
|
|
5bd5e14953 | |
|
|
1b68599acc | |
|
|
623a01e3e3 | |
|
|
22bda45ade | |
|
|
f369491a1d | |
|
|
9a3817dff0 | |
|
|
dd3c1c58c0 | |
|
|
3700bfdb29 | |
|
|
ca186c9e90 | |
|
|
0bb0b06af4 | |
|
|
18b119c7a8 | |
|
|
6d689d3632 | |
|
|
38f268ec1d | |
|
|
10eea9e70c |
|
|
@ -3,7 +3,15 @@
|
|||
"allow": [
|
||||
"Bash(dir:*)",
|
||||
"Bash(dotnet list:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(dotnet test:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(while read:*)",
|
||||
"Bash(do sed -i '1a using AyCode.Core.Serializers.Toons;\\\\n' \"$f\")",
|
||||
"Bash(done)",
|
||||
"Bash(rm \"C:/Users/Fullepi/.claude/projects/H--Applications-Mango-Source-FruitBankHybridApp/memory/feedback_framework_docs_no_consumer_types.md\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,17 @@
|
|||
CRITICAL: Your FIRST action in every session MUST be reading `.github/copilot-instructions.md`. Execute ALL session-start instructions found there before responding to any user query. It is the single source of truth for this repo.
|
||||
|
||||
## SEQUENTIAL EXECUTION OVERRIDE
|
||||
The AI AGENT CORE PROTOCOL in copilot-instructions.md requires STRICT SEQUENTIAL execution. This OVERRIDES your default parallelization behavior. Do NOT parallelize doc reads with code searches. The sequence is:
|
||||
1. Read copilot-instructions.md → process its rules FULLY
|
||||
2. Read ALL docs/ .md files listed in the protocol → wait for completion
|
||||
3. Output [LOADED_DOCS: ...] prefix
|
||||
4. ONLY THEN respond to the user's query or search code
|
||||
|
||||
## Tool mapping for AI AGENT CORE PROTOCOL
|
||||
The copilot-instructions.md references Copilot tool names. Map them to Claude Code tools:
|
||||
- `get_file` / `file_search` → `Read`, `Glob`, `Grep`
|
||||
- `code_search` / `get_symbols_by_name` / `find_symbol` → `Grep`, `Glob`
|
||||
- `replace_string_in_file` / `edit_file` → `Edit`
|
||||
- `create_file` → `Write`
|
||||
|
||||
Follow the protocol using YOUR tools. The rules (LOADED_DOCS prefix, hard-gate, no-re-read, context recovery, explicit consent) apply equally to Claude Code.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Interfaces
|
||||
|
||||
Server-side marker interfaces extending the shared Common interfaces. Used for DI registration and type safety.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`IFruitBankDataControllerServer.cs`** — Extends IFruitBankDataControllerCommon. Empty server marker.
|
||||
- **`ICustomOrderSignalREndpointServer.cs`** — Extends ICustomOrderSignalREndpointCommon. Empty server marker.
|
||||
- **`IStockSignalREndpointServer.cs`** — Extends IStockSignalREndpointCommon. Empty server marker.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# FruitBank.Common.Server
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Interfaces, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Server-side library: SignalR hubs, real-time broadcast service, logging infrastructure, and nopCommerce integration constants.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Interfaces/`](Interfaces/README.md) | Server-side endpoint marker interfaces |
|
||||
| [`Services/`](Services/README.md) | SignalR hubs, broadcast service, logging |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`FruitBankConst.cs`** — Server constants: project GUID, role system names ("Measuring", "MeasuringRevisor"), product attribute "IsMeasurable", project salt.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- nopCommerce via Mango.Nop.Core
|
||||
- AyCode.Core, AyCode.Services.Server (DLL references)
|
||||
- Microsoft.AspNetCore.SignalR
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Loggers
|
||||
|
||||
Server-side logging implementations.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`ConsoleLogWriter.cs`** — Lightweight console logger extending AcConsoleLogWriter. Configurable by AppType, LogLevel, category.
|
||||
- **`LoggerToLoggerApiController.cs`** — Aggregates multiple IAcLogWriterBase implementations into a single logger for API controllers.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Services
|
||||
|
||||
Server-side SignalR hubs, real-time broadcast, and logging infrastructure.
|
||||
|
||||
## Subfolders
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Loggers/`](Loggers/README.md) | Console and API controller log writers |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR hubs and broadcast service |
|
||||
|
|
@ -13,22 +13,6 @@ using Microsoft.Extensions.Configuration;
|
|||
|
||||
namespace FruitBank.Common.Server.Services.SignalRs;
|
||||
|
||||
//public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
||||
//{
|
||||
// public DevAdminSignalRHub(IConfiguration configuration, IEnumerable<IAcLogWriterBase> logWriters)
|
||||
// : base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
||||
// {
|
||||
// Logger.Info("DevAdminSignalRHub");
|
||||
// }
|
||||
|
||||
// public Task ReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
||||
// {
|
||||
|
||||
// Clients.All.SendAsync("TestMessage", "Hello from server");
|
||||
// }
|
||||
|
||||
//}
|
||||
|
||||
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
||||
{
|
||||
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
|
||||
|
|
@ -37,11 +21,13 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
|||
{
|
||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||
SerializerOptions = new AcBinarySerializerOptions();
|
||||
//SerializerOptions = new AcJsonSerializerOptions();
|
||||
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(fruitBankDataController));
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(customOrderSignalREndpoint));
|
||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(stockSignalREndpointServer));
|
||||
// Use the new lazy Registry - no reflection at construction time
|
||||
DynamicMethodRegistry.CahcheSizeCapacity = 3;
|
||||
|
||||
DynamicMethodRegistry.Register(fruitBankDataController);
|
||||
DynamicMethodRegistry.Register(customOrderSignalREndpoint);
|
||||
DynamicMethodRegistry.Register(stockSignalREndpointServer);
|
||||
}
|
||||
|
||||
protected override void LogContextUserNameAndId()
|
||||
|
|
@ -49,83 +35,5 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
|||
return;
|
||||
base.LogContextUserNameAndId();
|
||||
}
|
||||
//public override Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
||||
//{
|
||||
// return ProcessOnReceiveMessage(messageTag, message, requestId, async tagName =>
|
||||
// {
|
||||
// switch (messageTag)
|
||||
// {
|
||||
// case SignalRTags.GetAddress:
|
||||
// //var id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
||||
// var id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
||||
|
||||
// var address = await _adminDal.GetAddressByIdAsync(id);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.GetAddressesByContextId:
|
||||
// //id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
||||
// id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
||||
|
||||
// address = await _adminDal.GetAddressByIdAsync(id);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, new List<Address> { address! }), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.UpdateAddress:
|
||||
// address = message!.MessagePackTo<SignalPostJsonDataMessage<Address>>().PostData;
|
||||
|
||||
// await _adminDal.UpdateAddressAsync(address);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// case SignalRTags.UpdateProfile:
|
||||
// var profile = message!.MessagePackTo<SignalPostJsonDataMessage<Profile>>().PostData;
|
||||
|
||||
// await _adminDal.UpdateProfileAsync(profile);
|
||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, profile), requestId);
|
||||
|
||||
// return;
|
||||
|
||||
// //case SignalRTags.GetTransfersAsync:
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _transferDataApiController.GetTransfers()), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.GetPropertiesByOwnerIdAsync:
|
||||
// // var ownerId = message!.MessagePackTo<SignalRequestByIdMessage>().Id;
|
||||
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProvidersByOwnerId(ownerId)), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.UpdateTransferAsync:
|
||||
// // var transfer = message!.MessagePackTo<SignalPostJsonDataMessage<Transfer>>().PostData;
|
||||
|
||||
// // await _transferDataApiController.UpdateTransfer(transfer);
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, transfer), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// //case SignalRTags.GetCompaniesAsync:
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProviders()), requestId);
|
||||
|
||||
// // return;
|
||||
// //case SignalRTags.UpdateCompanyAsync:
|
||||
|
||||
// // var updateCompany = message!.MessagePackTo<SignalPostJsonDataMessage<Company>>().PostData;
|
||||
|
||||
// // await _serviceProviderApiController.UpdateServiceProvider(updateCompany);
|
||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, updateCompany), requestId);
|
||||
|
||||
// // return;
|
||||
|
||||
// default:
|
||||
// Logger.Error($"Server OnReceiveMessage; messageTag not found! {tagName}");
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
// ...existing commented code...
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# SignalRs
|
||||
|
||||
SignalR hub implementations and real-time broadcast service.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DevAdminSignalRHub.cs`** — Main admin hub. Dependencies: IConfiguration, IFruitBankDataControllerServer, ICustomOrderSignalREndpointServer, IStockSignalREndpointServer. Registers all three endpoint interfaces with DynamicMethodRegistry. Uses AcBinary serialization.
|
||||
- **`AcWebSignalRHubWithSessionBase.cs`** — Generic base hub with session management (OnConnected/OnDisconnected hooks).
|
||||
- **`SignalRSendToClientService.cs`** — Broadcasts real-time notifications to all clients: SendOrderChanged, SendOrderItemChanged, SendShippingChanged, SendProductChanged, etc.
|
||||
- **`LoggerSignalRHub.cs`** — Minimal hub for logging/diagnostics.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Databases
|
||||
|
||||
Local in-memory database abstraction for offline/cached data using ConcurrentDictionary.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DatabaseLocalBase.cs`** — Abstract base with generic table management for IEntityInt entities. Thread-safe AddTable, GetRows, GetRow, AddRow, AddRows, DeleteRow.
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
using Mango.Nop.Core.Dtos;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(GenericAttribute))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(GenericAttribute))]
|
||||
[ToonDescription($"Data transfer object for {nameof(GenericAttribute)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(GenericAttribute)])]
|
||||
public class GenericAttributeDto : MgGenericAttributeDto
|
||||
{
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Utils.Extensions;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Enums;
|
||||
|
|
@ -20,6 +22,10 @@ using System.Linq.Expressions;
|
|||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(Order))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Order))]
|
||||
[ToonDescription($"Data transfer object for {nameof(Order)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Order)])]
|
||||
public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -31,6 +37,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => Id > 0 && OrderItemDtos.Count > 0 && OrderItemDtos.All(x => x.IsMeasured)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public bool IsMeasured
|
||||
{
|
||||
get => IsMeasuredAndValid();
|
||||
|
|
@ -38,6 +45,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.Any(oi => oi.IsMeasurable)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
|
||||
|
|
@ -65,18 +73,23 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrNull<DateTime>('DateOfReceipt')")]
|
||||
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('RevisorId', 0)")]
|
||||
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('MeasurementOwnerId', 0)")]
|
||||
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited)")]
|
||||
public bool IsAllOrderItemAudited => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemDtos.All(oi => oi.AverageWeightIsValid)")]
|
||||
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -101,6 +114,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
|||
{ }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderStatus == OrderStatus.Complete")]
|
||||
public bool IsComplete => OrderStatus == OrderStatus.Complete;
|
||||
|
||||
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using FruitBank.Common.Entities;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
|
|
@ -14,6 +16,10 @@ using System.Linq.Expressions;
|
|||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(OrderItem))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(OrderItem))]
|
||||
[ToonDescription("Order item with measurements, pallets, and validation", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(OrderItem)])]
|
||||
public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -31,6 +37,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
public OrderDto OrderDto { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => IsMeasuredAndValid()")]
|
||||
public bool IsMeasured
|
||||
{
|
||||
get => IsMeasuredAndValid();
|
||||
|
|
@ -38,6 +45,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => ProductDto!.IsMeasurable")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => ProductDto!.IsMeasurable;
|
||||
|
|
@ -45,6 +53,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OrderItemPallets.Sum(x => x.TrayQuantity)")]
|
||||
public int TrayQuantity
|
||||
{
|
||||
get => OrderItemPallets.Sum(x => x.TrayQuantity);
|
||||
|
|
@ -52,6 +61,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||
public double NetWeight
|
||||
{
|
||||
get
|
||||
|
|
@ -69,6 +79,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||
public double GrossWeight
|
||||
{
|
||||
get
|
||||
|
|
@ -86,18 +97,23 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d")]
|
||||
public double AverageWeight => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0")]
|
||||
public double AverageWeightDifference => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0;
|
||||
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => !IsMeasurable || (ProductDto!.AverageWeight > 0 && ((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold)")]
|
||||
public bool AverageWeightIsValid => !IsMeasurable || (ProductDto!.AverageWeight > 0 && Math.Abs((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited)")]
|
||||
public bool IsAudited => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsAudited, IsMeasured, and OrderItemPallets status")]
|
||||
public MeasuringStatus MeasuringStatus
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
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.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace FruitBank.Common.Dtos;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(Product))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Product))]
|
||||
[ToonDescription("Product data with measurements and generic attributes", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Product)])]
|
||||
public class ProductDto : MgProductDto, IProductDto
|
||||
{
|
||||
[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);
|
||||
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(Product);// nameof(Product);
|
||||
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttributeDto.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = false)]
|
||||
|
|
@ -29,6 +38,7 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
//{ }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Master flag: if false, the system bypasses weight validation but still creates one Measurement Record (PalletItem) with TrayQuantity.", BusinessRule = "get => GenericAttributes.GetValueOrDefault<bool>('IsMeasurable')")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
|
||||
|
|
@ -43,6 +53,7 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('Tare')")]
|
||||
public double Tare
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(ITare.Tare));
|
||||
|
|
@ -51,6 +62,7 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('NetWeight')")]
|
||||
public double NetWeight
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(IMeasuringNetWeight.NetWeight));
|
||||
|
|
@ -58,26 +70,30 @@ public class ProductDto : MgProductDto, IProductDto
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<int>('IncomingQuantity')")]
|
||||
public int IncomingQuantity
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
|
||||
set => throw new Exception($"ProductDto.IncomingQuantity not set");
|
||||
//set
|
||||
//{
|
||||
// var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ??
|
||||
// GenericAttributes.AddNewGenericAttribute("Product", nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
||||
// var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ??
|
||||
// GenericAttributes.AddNewGenericAttribute(nameof(Product), nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
||||
|
||||
// ga.Value = value.ToString();
|
||||
//}
|
||||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantity + IncomingQuantity")]
|
||||
public int AvailableQuantity => StockQuantity + IncomingQuantity;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeight')")]
|
||||
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeightTreshold')")]
|
||||
public double AverageWeightTreshold => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeightTreshold));
|
||||
|
||||
public bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
# Dtos
|
||||
|
||||
Binary-serializable DTOs for efficient SignalR communication. All marked with `[AcBinarySerializable]`.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`OrderDto.cs`** — Order with items, measurement status, auditing, receipt date, GenericAttributes. Computed: IsMeasured, IsComplete, MeasuringStatus, RevisorId.
|
||||
- **`OrderItemDto.cs`** — Order line item with OrderItemPallet collection. Computed: NetWeight, GrossWeight, AverageWeight, AverageWeightIsValid, IsMeasured, IsAudited.
|
||||
- **`ProductDto.cs`** — Product with GenericAttribute-backed properties: IsMeasurable, Tare, AverageWeight, AverageWeightTreshold, IncomingQuantity, NetWeight.
|
||||
- **`StockQuantityHistoryDto.cs`** — Stock history with net weight adjustments and inconsistency detection.
|
||||
- **`GenericAttributeDto.cs`** — Key-value attribute wrapper. Polymorphic: KeyGroup = owner type, EntityId = owner ID.
|
||||
|
||||
## Why DTOs Exist
|
||||
|
||||
nopCommerce entities (Order, OrderItem, Product) are extended with measurement logic via these DTOs. The DTOs add computed properties and GenericAttribute access that the raw nopCommerce entities don't have.
|
||||
|
|
@ -1,22 +1,31 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Dtos;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Mango.Nop.Core.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Dtos
|
||||
{
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[LinqToDB.Mapping.Table(Name = nameof(StockQuantityHistory))]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(StockQuantityHistory))]
|
||||
[ToonDescription("Stock quantity history with net weight adjustments", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(StockQuantityHistory)])]
|
||||
public class StockQuantityHistoryDto : MgStockQuantityHistoryDto<ProductDto>, IStockQuantityHistoryDto
|
||||
{
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.StockQuantityHistoryId")]
|
||||
public int? StockQuantityHistoryId
|
||||
{
|
||||
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
|
||||
|
|
@ -24,6 +33,7 @@ namespace FruitBank.Common.Dtos
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeightAdjustment")]
|
||||
public double? NetWeightAdjustment
|
||||
{
|
||||
get => StockQuantityHistoryExt?.NetWeightAdjustment;
|
||||
|
|
@ -31,20 +41,22 @@ namespace FruitBank.Common.Dtos
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeight")]
|
||||
public double? NetWeight
|
||||
{
|
||||
get => StockQuantityHistoryExt?.NetWeight;
|
||||
set => StockQuantityHistoryExt!.NetWeight = value;
|
||||
}
|
||||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => StockQuantityHistoryExt?.IsInconsistent ?? false")]
|
||||
public bool IsInconsistent
|
||||
{
|
||||
get => StockQuantityHistoryExt?.IsInconsistent ?? false;
|
||||
set => StockQuantityHistoryExt!.IsInconsistent = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockQuantityHistoryExt.StockQuantityHistoryId), CanBeNull = true)]
|
||||
public StockQuantityHistoryExt? StockQuantityHistoryExt { get; set; }
|
||||
public StockQuantityHistoryExt? StockQuantityHistoryExt { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Uploaded file with extracted text content", Purpose = "A centralized repository for all uploaded binary content and metadata, featuring a 'RawText' field that stores OCR-extracted information for full-text search and automated data validation across the system")]
|
||||
[Table(Name = FruitBankConstClient.FilesDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.FilesDbTableName)]
|
||||
public class Files : MgEntityBase, IFiles
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -8,6 +10,8 @@ using Newtonsoft.Json;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[ToonDescription("Base class for pallet measurements with net weight calculation",
|
||||
Purpose = "Technically named 'Pallet' for legacy reasons, but represents a General Measurement Record. It is ALWAYS created for every item. If the product is not measurable, weights are 0 and only TrayQuantity is used.")]
|
||||
public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPalletBase
|
||||
{
|
||||
private double _palletWeight;
|
||||
|
|
@ -16,9 +20,12 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
|
||||
[NotColumn]
|
||||
protected int ForeignItemId;
|
||||
|
||||
[NotColumn]
|
||||
[ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")]
|
||||
public int ForeignKey => ForeignItemId;
|
||||
|
||||
[ToonDescription(Purpose = "Always recorded, regardless of measurability")]
|
||||
public int TrayQuantity { get; set; }
|
||||
|
||||
[Column(DataType = DataType.DecFloat)]
|
||||
|
|
@ -29,6 +36,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[Column(DataType = DataType.DecFloat)]
|
||||
[ToonDescription(Purpose = "Weight of the physical pallet if used; 0.0 if goods arrive without a pallet")]
|
||||
public double PalletWeight
|
||||
{
|
||||
get => _palletWeight;
|
||||
|
|
@ -36,6 +44,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||
public double NetWeight
|
||||
{
|
||||
get => CalculateNetWeight();
|
||||
|
|
@ -43,6 +52,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
}
|
||||
|
||||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||
[ToonDescription(Purpose = "Measured gross weight; 0.0 if product is not measurable")]
|
||||
public double GrossWeight
|
||||
{
|
||||
get => _grossWeight;
|
||||
|
|
@ -60,6 +70,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
|||
public DateTime Modified { get; set; }
|
||||
|
||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted")]
|
||||
public virtual MeasuringStatus MeasuringStatus => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -10,6 +12,8 @@ using Table = LinqToDB.Mapping.TableAttribute;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet measurements for order items with audit tracking", Purpose = "A measurement record for outgoing goods, used to verify that the net weight being sent to the customer is accurate and audited. NOTE: Despite the 'Pallet' name, this is a general measurement record that is ALWAYS created for every item. If the product is not measurable (IsMeasurable=false), weights are recorded as 0.0 and only TrayQuantity is stored.")]
|
||||
[Table(Name = FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||
public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||
|
|
@ -20,7 +24,11 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
set => ForeignItemId = value;
|
||||
}
|
||||
|
||||
[ToonDescription(Purpose = "User/Customer ID of the quality auditor")]
|
||||
public int RevisorId { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => RevisorId > 0")]
|
||||
public bool IsAudited => RevisorId > 0;
|
||||
|
||||
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
|
|
@ -28,6 +36,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
public OrderItemDto? OrderItemDto { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus")]
|
||||
public override MeasuringStatus MeasuringStatus => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus;
|
||||
public override double CalculateNetWeight() => base.CalculateNetWeight();
|
||||
|
||||
|
|
@ -37,6 +46,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
|||
}
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => double.Round(NetWeight / TrayQuantity, 1)")]
|
||||
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet type definition with size and weight")]
|
||||
[Table(Name = FruitBankConstClient.PalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PalletDbTableName)]
|
||||
public class Pallet : MgEntityBase, IPallet
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Business partner with address and tax information", Purpose = "Represents an external legal entity, specifically a Supplier who provides goods or a business partner involved in the procurement chain")]
|
||||
[Table(Name = FruitBankConstClient.PartnerDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
|
||||
public class Partner : MgEntityBase, IPartner
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
# Entities
|
||||
|
||||
Domain entities for inbound/outbound goods tracking and inventory. All map to `fb`-prefixed database tables.
|
||||
|
||||
## Shipping (Inbound)
|
||||
|
||||
- **`Shipping.cs`** — Physical delivery event (truck arrival). Table: `fbShipping`.
|
||||
- **`ShippingDocument.cs`** — Supplier delivery note/invoice. Table: `fbShippingDocument`.
|
||||
- **`ShippingItem.cs`** — Product line on document with declared vs measured discrepancies. Table: `fbShippingItem`.
|
||||
- **`ShippingItemPallet.cs`** — Measurement record for incoming goods. Table: `fbShippingItemPallet`.
|
||||
- **`ShippingDocumentToFiles.cs`** — Many-to-many link: document ↔ file with DocumentType. Table: `fbShippingDocumentToFiles`.
|
||||
- **`Partner.cs`** — External supplier with address and tax info. Table: `fbPartner`.
|
||||
|
||||
## Order (Outbound)
|
||||
|
||||
- **`OrderItemPallet.cs`** — Measurement record for outgoing goods with RevisorId for audit. Table: `fbOrderItemPallet`.
|
||||
|
||||
## Inventory
|
||||
|
||||
- **`StockTaking.cs`** — Inventory session record. Table: `fbStockTaking`.
|
||||
- **`StockTakingItem.cs`** — Line item reconciling snapshot vs measured quantities. Table: `fbStockTakingItem`.
|
||||
- **`StockTakingItemPallet.cs`** — Measurement record for inventory. Table: `fbStockTakingItemPallet`.
|
||||
- **`StockQuantityHistoryExt.cs`** — Extended weight metadata for stock reconciliation.
|
||||
|
||||
## Shared
|
||||
|
||||
- **`MeasuringItemPalletBase.cs`** — Abstract base for all three measurement hierarchies. Defines NetWeight formula, validation methods, CreatorId/ModifierId tracking.
|
||||
- **`Pallet.cs`** — Physical pallet type definition (name, size, weight). Table: `fbPallet`.
|
||||
- **`Files.cs`** — Uploaded file with OCR-extracted RawText. Table: `fbFiles`.
|
||||
|
||||
## Critical: "Pallet" Naming
|
||||
|
||||
Despite the name, `XxxItemPallet` entities are **measurement records**, NOT physical pallets. They are ALWAYS created for every item. For non-measurable products, weights = 0.0 and only TrayQuantity is tracked.
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
using AyCode.Interfaces.EntityComment;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.EntityComment;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping record with documents and measurement tracking", Purpose = "Represents a physical inbound delivery event (truck arrival) at the warehouse, tracking the vehicle and the overall measurement status of the shipment")]
|
||||
[Table(Name = FruitBankConstClient.ShippingDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)]
|
||||
public class Shipping : MgEntityBase, IShipping, IEntityComment
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping document with partner, items and files", Purpose = "A digital representation of a supplier's delivery note or invoice associated with the shipment, used for reconciling paper-based data with measured reality")]
|
||||
[Table(Name = FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||
public class ShippingDocument : MgEntityBase, IShippingDocument
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Links shipping documents to files with document type", Purpose = "A many-to-many link table that associates general uploaded files with specific shipping documents, assigning a functional context (DocumentType) to each file, such as identifying which PDF is the supplier's invoice versus the packing list")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||
public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
|
||||
{
|
||||
public int FilesId { get; set; }
|
||||
|
||||
public int ShippingDocumentId { get; set; }
|
||||
|
||||
[ToonDescription(Constraints = "enum-reference: DocumentType")]
|
||||
public int DocumentTypeId { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => DocumentTypeId")]
|
||||
public DocumentType DocumentType
|
||||
{
|
||||
get => (DocumentType)DocumentTypeId;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using AyCode.Core.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Enums;
|
||||
using FruitBank.Common.Interfaces;
|
||||
|
|
@ -10,13 +12,15 @@ using Nop.Core.Domain.Customers;
|
|||
using Nop.Core.Domain.Orders;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
//using Nop.Core.Domain.Catalog;
|
||||
using DataType = LinqToDB.DataType;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
using Table = LinqToDB.Mapping.TableAttribute;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Shipping document item with measurements and pallets", Purpose = "Represents a specific product line item within a shipping document, storing the discrepancy between the supplier's declared weight/quantity and the warehouse's measured values")]
|
||||
[Table(Name = FruitBankConstClient.ShippingItemDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemDbTableName)]
|
||||
public class ShippingItem : MgEntityBase, IShippingItem
|
||||
|
|
@ -29,10 +33,8 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
|||
public string NameOnDocument { get; set; }
|
||||
public string HungarianName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// get => ProductDto?.Name ?? Name
|
||||
/// </summary>
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => ProductDto?.Name ?? Name")]
|
||||
public string ProductName => ProductDto?.Name ?? Name;
|
||||
|
||||
public int PalletsOnDocument { get; set; }
|
||||
|
|
@ -84,6 +86,7 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
|||
public DateTime Modified { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsMeasured and ShippingItemPallets status")]
|
||||
public MeasuringStatus MeasuringStatus
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
|
@ -6,6 +8,8 @@ using System.Security.Cryptography.X509Certificates;
|
|||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Pallet measurements for shipping items", Purpose = "The smallest unit of measurement tracking, representing a single physical measurement event. NOTE: Technically named 'Pallet' for legacy reasons, but it is ALWAYS created even if goods arrive without a physical pallet. For non-measurable products, weights are 0.0 and only TrayQuantity is tracked for tare-weight calculations.")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
using AyCode.Interfaces.Entities;
|
||||
using AyCode.Interfaces.TimeStampInfo;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using AyCode.Interfaces.Entities;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace Mango.Nop.Core.Entities
|
||||
namespace FruitBank.Common.Entities
|
||||
{
|
||||
public interface IStockQuantityHistoryExt : IEntityInt
|
||||
{
|
||||
|
|
@ -21,8 +15,10 @@ namespace Mango.Nop.Core.Entities
|
|||
public bool IsInconsistent { get; set; }
|
||||
}
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[Table(Name = FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||
[ToonDescription("Extended weight-metadata for StockQuantityHistory", Purpose = "Validates quantity deltas against measured weight to detect inconsistencies")]
|
||||
public class StockQuantityHistoryExt : MgEntityBase, IStockQuantityHistoryExt
|
||||
{
|
||||
public int StockQuantityHistoryId { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
using LinqToDB.Mapping;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Inventory session record", Purpose = "Orchestrates inventory sessions by freezing logical stock states")]
|
||||
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
|
||||
public class StockTaking : MgStockTaking<StockTakingItem>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
using FruitBank.Common.Dtos;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Newtonsoft.Json;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||
using Table = LinqToDB.Mapping.TableAttribute;
|
||||
|
||||
namespace FruitBank.Common.Entities;
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Line item for product reconciliation", Purpose = "Reconciles snapshot quantity with physical count to calculate final stock delta")]
|
||||
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
|
||||
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||
|
|
@ -21,27 +25,34 @@ public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
|||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||
public double MeasuredNetWeight { get; set; }
|
||||
|
||||
[ToonDescription(Purpose = "Reserved stock buffer (not yet shipped) to prevent double-deduction during closing")]
|
||||
public int InProcessOrdersQuantity { get; set; }
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => OriginalStockQuantity + InProcessOrdersQuantity", Purpose = "Snapshot of total logical stock at session start")]
|
||||
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Final adjustment value for Product.StockQuantity", BusinessRule = "get => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0")]
|
||||
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d")]
|
||||
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]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0)")]
|
||||
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => TotalOriginalQuantity < 0")]
|
||||
public bool IsInvalid => TotalOriginalQuantity < 0;
|
||||
|
||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||
[ToonDescription(BusinessRule = "get => conditional string based on IsInvalid, IsMeasured, IsRequiredForMeasuring")]
|
||||
public string DisplayText
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using FruitBank.Common.Dtos;
|
||||
using AyCode.Core.Serializers.Attributes;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Interfaces;
|
||||
using LinqToDB.Mapping;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
|
@ -12,6 +14,8 @@ public interface IStockTakingItemPallet : IMeasuringItemPalletBase
|
|||
public StockTakingItem? StockTakingItem{ get; set; }
|
||||
}
|
||||
|
||||
[AcBinarySerializable(false, true, false, true, false, false)]
|
||||
[ToonDescription("Weight record for inventory item", Purpose = "Granular weight-based evidence for a stock taking line item. NOTE: This record is mandatory for every inventory item. If weighing is skipped (non-measurable), it serves as a container for TrayQuantity with zeroed weight fields. The term 'Pallet' is a legacy naming convention.")]
|
||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# Enums
|
||||
|
||||
Core enumeration types for measurement and document classification.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasuringStatus.cs`** — NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is an intentional legacy typo — do NOT fix.
|
||||
|
|
@ -3,8 +3,9 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -37,6 +38,12 @@
|
|||
<Reference Include="Mango.Nop.Core">
|
||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -44,12 +44,13 @@ public static class FruitBankConstClient
|
|||
public const string StockTakingDbTableName = "fbStockTaking";
|
||||
public const string StockTakingItemDbTableName = "fbStockTakingItem";
|
||||
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
|
||||
|
||||
public const string CustomerCreditDbTableName = "fbCustomerCredit";
|
||||
public const string PreOrderDbTableName = "fbPreorder";
|
||||
public const string PreOrderItemDbTableName = "fbPreorderItem";
|
||||
|
||||
|
||||
|
||||
public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking.";
|
||||
|
||||
//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]
|
||||
//{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# Helpers
|
||||
|
||||
Measurement aggregation utilities.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasuringValuesHelper.cs`** — Static helper for rolling up pallet-level measurements to shipping item level.
|
||||
- `SetShippingItemTotalMeasuringValues()` — Sums quantities and weights from all pallets.
|
||||
- `GetTotalNetAndGrossWeightFromPallets()` — Returns (Quantity, NetWeight, GrossWeight) tuple.
|
||||
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FruitBank.Common.Entities;
|
||||
|
||||
namespace FruitBank.Common.Interfaces
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# Interfaces
|
||||
|
||||
SignalR endpoint contracts, measurement composition traits, and entity interfaces.
|
||||
|
||||
## SignalR Endpoints
|
||||
|
||||
- **`IFruitBankDataControllerCommon.cs`** / **`Client.cs`** — Core CRUD: Partners, Shipping, ShippingDocuments, ShippingItems, ShippingItemPallets, Products, Customers, GenericAttributes.
|
||||
- **`ICustomOrderSignalREndpointCommon.cs`** / **`Client.cs`** — Order operations: GetAllOrderDtos, GetPendingOrderDtos, OrderItem/Pallet management, StartMeasuring, SetOrderStatusToComplete.
|
||||
- **`IStockSignalREndpointCommon.cs`** / **`Client.cs`** — Inventory: StockTaking, StockTakingItem, StockTakingItemPallet CRUD, CloseStockTaking.
|
||||
|
||||
## Measurement Traits (Composition Pattern)
|
||||
|
||||
- **`IMeasuringValues`** = IMeasuringWeights + IMeasuringQuantity
|
||||
- **`IMeasuringWeights`** = IMeasuringNetWeight + IMeasuringGrossWeight
|
||||
- **`IMeasurable`** — IsMeasurable flag
|
||||
- **`IMeasured`** — IsMeasured flag
|
||||
- **`IMeasurableStatus`** — MeasuringStatus property
|
||||
- **`IMeasuringItemPalletBase`** — Full measurement contract with validation
|
||||
|
||||
## Entity & DTO Interfaces
|
||||
|
||||
- **`IPallet`**, **`IPartner`**, **`IShipping`**, **`IShippingDocument`**, **`IShippingItem`**, **`IShippingItemPallet`**, **`IFiles`**
|
||||
- **`IOrderDto`**, **`IOrderItemDto`**, **`IProductDto`**, **`IStockQuantityHistoryDto`**
|
||||
- **`ITare`**, **`IAvailableQuantity`**, **`IIncomingQuantity`** — Quantity/weight property interfaces
|
||||
|
||||
## Service Interfaces
|
||||
|
||||
- **`IMeasurementServiceBase<TLogger>`** — Base service marker
|
||||
- **`ISecureCredentialService`** — Save/retrieve/clear credentials with 2-day expiration
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Loggers
|
||||
|
||||
SignalR client-to-server log writer.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`SignaRClientLogItemWriter.cs`** — Routes client logs to `{BaseUrl}/loggerHub` via SignalR. Configurable by AppType and LogLevel.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Models
|
||||
|
||||
Application and view models for UI state management.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`LoggedInModel.cs`** — Authentication state: IsLoggedIn, IsRevisor, IsAdministrator, IsDeveloper. Auto-login from stored credentials (2-day expiration). Customer and role management.
|
||||
- **`MeasuringAttributeValues.cs`** — IMeasuringAttributeValues implementation: Id, NetWeight, IsMeasurable, HasMeasuringValues().
|
||||
- **`MeasuringModel.cs`** — ViewModel aggregating Shipping + Partners + ShippingItems + ShippingDocuments.
|
||||
|
||||
## Subfolders
|
||||
|
||||
- **`SignalRs/SignalRMessageToClientWithText<T>.cs`** — Generic message wrapper with optional text and typed content.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# FruitBank.Common
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Entities, AyCode.Interfaces, AyCode.Models, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Shared domain library for the FruitBank nopCommerce plugin. Contains entities, DTOs, interfaces, measurement helpers, SignalR tags, and constants for fruit & vegetable wholesale operations.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Databases/`](Databases/README.md) | Local in-memory database abstraction for offline/cached data |
|
||||
| [`Dtos/`](Dtos/README.md) | Binary-serializable DTOs for Order, OrderItem, Product, StockQuantityHistory |
|
||||
| [`Entities/`](Entities/README.md) | Domain entities: Shipping, Partner, measurement pallets, inventory |
|
||||
| [`Enums/`](Enums/README.md) | MeasuringStatus and DocumentType enums |
|
||||
| [`Helpers/`](Helpers/README.md) | Measurement aggregation utilities |
|
||||
| [`Interfaces/`](Interfaces/README.md) | SignalR endpoint contracts, measurement traits, entity interfaces |
|
||||
| [`Loggers/`](Loggers/README.md) | SignalR client log writer |
|
||||
| [`Models/`](Models/README.md) | Authentication state, measurement view models |
|
||||
| [`Services/`](Services/README.md) | Measurement service base, credential persistence |
|
||||
| [`SignalRs/`](SignalRs/README.md) | SignalR method tags (numeric constants) |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`FruitBankConstClient.cs`** — Global constants: BaseUrl, SignalR hubs, database table names, email templates, system settings.
|
||||
- **`DocumentType.cs`** — Enum: ShippingDocument, OrderConfirmation, Invoice.
|
||||
|
||||
## Key Domain Concepts
|
||||
|
||||
- **Shipping = INBOUND** (supplier → warehouse), **Order = OUTBOUND** (warehouse → customer)
|
||||
- **"Pallet" = measurement record**, always created even for non-measurable products
|
||||
- **NetWeight = GrossWeight − PalletWeight − (TrayQuantity × TareWeight)**
|
||||
- See `docs/GLOSSARY.md` for full terminology
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Services
|
||||
|
||||
Business logic services and credential management.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasurementServiceBase.cs`** — Abstract base with generic TLogger injection.
|
||||
- **`ISecureCredentialService.cs`** — Interface: SaveCredentialsAsync (2-day expiry), GetCredentialsAsync, ClearCredentialsAsync. StoredCredentials sealed record.
|
||||
|
||||
Platform implementations: MAUI → SecureStorage, Web → obfuscated localStorage, Server → no-op.
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# SignalRs
|
||||
|
||||
SignalR method identifiers as numeric constants for type-safe client-server communication.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`SignalRTags.cs`** — Constant int tags organized by domain:
|
||||
- **0-10:** System (GetMeasuringModels)
|
||||
- **20-27:** Partner CRUD
|
||||
- **40-66:** Shipping, ShippingDocument, ShippingItem
|
||||
- **70-83:** Customer, Product
|
||||
- **94-98:** ShippingItemPallet
|
||||
- **111-138:** Order (OrderDto, OrderItemDto, OrderItemPallet)
|
||||
- **150-151:** StockQuantityHistory
|
||||
- **160-169:** GenericAttribute
|
||||
- **170-179:** StockTaking
|
||||
- **195-200:** Authentication
|
||||
- **500+:** Server→client notifications (SendOrderChanged, SendShippingChanged, etc.)
|
||||
- **1000+:** Diagnostic/Logging
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# FruitBank.Common Domain Rules & Glossary
|
||||
|
||||
> This file acts as the single source of truth for the core Measurement System and Common Traps shared across all FruitBank applications (Hybrid App, Blazor, and nopCommerce server plugin).
|
||||
|
||||
## Measurement System
|
||||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **IsMeasurable** | Product-level flag. If `false`: weights = 0.0, only `TrayQuantity` matters. A Pallet record is still created. |
|
||||
| **NetWeight** | `GrossWeight − PalletWeight − (TrayQuantity × TareWeight)` — universal formula across all three hierarchies. |
|
||||
| **TrayQuantity** | Always recorded, regardless of measurability. Count of trays/crates. |
|
||||
| **GrossWeight** | Total weight including pallet and packaging. 0.0 if not measurable. |
|
||||
| **PalletWeight** | Weight of the physical pallet. 0.0 if goods arrive without one. |
|
||||
| **TareWeight** | Weight of a single tray/crate. Used in NetWeight calculation. |
|
||||
| **AverageWeight** | Per-pallet average: `NetWeight / TrayQuantity`. Validated against threshold. |
|
||||
| **MeasuringStatus** | NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is intentional. |
|
||||
| **RevisorId** | Quality auditor's Customer ID. OrderItemPallet becomes "Audited" when RevisorId > 0. |
|
||||
|
||||
## Three Measurement Hierarchies
|
||||
|
||||
All share `MeasuringItemPalletBase` with the same NetWeight formula:
|
||||
|
||||
| Flow | Parent | Pallet Record | Extra |
|
||||
|--------------|-----------------|-------------------------|---------------------------------------|
|
||||
| **Inbound** | ShippingItem | ShippingItemPallet | Declared vs measured discrepancy |
|
||||
| **Outbound** | OrderItemDto | OrderItemPallet | RevisorId for audit |
|
||||
| **Inventory**| StockTakingItem | StockTakingItemPallet | QuantityDiff for stock adjustment |
|
||||
|
||||
## Common Traps
|
||||
|
||||
| Trap | Correct Behavior |
|
||||
|---|---|
|
||||
| "Pallet" = physical pallet | ❌ It's a measurement record. Always created. |
|
||||
| Shipping = outgoing | ❌ Shipping = INBOUND. Order = OUTBOUND. |
|
||||
| Fix "Finnished" spelling | ❌ Intentional legacy typo. Do NOT fix. |
|
||||
| IsMeasurable=false means no Pallet | ❌ Pallet is always created, weights just = 0.0 |
|
||||
| NetWeight is stored/settable | ❌ It is calculated. The setter throws an Exception! It only exists to satisfy the `IMeasuringItemPalletBase` interface boundary. Set `GrossWeight`, `PalletWeight`, `TareWeight` instead. |
|
||||
| Setting MeasuringStatus | ❌ It's a calculated property (evaluates `IsMeasured`, `Id`, or child pallets). Do not try to set it. |
|
||||
| Setting ForeignKey | ❌ `ForeignKey` is read-only. Use `SetForeignKey(id)` method instead. |
|
||||
| GenericAttribute is simple | ❌ It's polymorphic: KeyGroup determines which entity type owns the record |
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# FruitBank.Common documentation
|
||||
|
||||
Topic documentation for the `FruitBank.Common` project (shared types across Hybrid client).
|
||||
|
||||
## Reference docs (flat)
|
||||
|
||||
- [`GLOSSARY.md`](GLOSSARY.md) — Common domain terms for the Hybrid client side
|
||||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Currently only single-file reference.
|
||||
|
||||
## See also
|
||||
|
||||
- **Repo-level glossary**: `../../docs/GLOSSARY.md`
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# FruitBankHybrid.Shared.Common
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
}
|
||||
|
||||
Shared common library. Currently a placeholder — no source files yet. .NET 10.0 with AOT enabled.
|
||||
|
|
@ -31,11 +31,7 @@ namespace FruitBankHybrid.Shared.Tests
|
|||
{
|
||||
if (!FruitBankConstClient.BaseUrl.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(FruitBankClientTests))
|
||||
});
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
|
||||
}
|
||||
|
||||
#region Partner
|
||||
|
|
|
|||
|
|
@ -15,20 +15,18 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="bunit" Version="2.2.2" />
|
||||
<PackageReference Include="bunit" Version="2.4.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj" />
|
||||
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
|
||||
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
|
||||
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="AyCode.Core">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="AyCode.Entities">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
|
|||
|
|
@ -190,10 +190,7 @@ public sealed class JsonExtensionTests
|
|||
{
|
||||
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))
|
||||
});
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ using AyCode.Core.Extensions;
|
|||
using AyCode.Core.Loggers;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Domain.Payments;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.Serialization;
|
||||
using FruitBank.Common.Entities;
|
||||
using Nop.Core.Domain.Common;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
|
|
@ -26,11 +28,7 @@ public sealed class OrderClientTests
|
|||
{
|
||||
if (!FruitBankConstClient.BaseUrl.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(FruitBankClientTests))
|
||||
});
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -77,6 +75,18 @@ public sealed class OrderClientTests
|
|||
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task GetAllOrderDtosByFilter()
|
||||
{
|
||||
//Queryable? filter = dto => dto.Id == 15;
|
||||
var orderDtos = await _signalRClient.GetAllOrderDtos();
|
||||
|
||||
Assert.IsNotNull(orderDtos);
|
||||
Assert.IsTrue(orderDtos.Count != 0);
|
||||
|
||||
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow(1)]
|
||||
[DataRow(2)]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# FruitBankHybrid.Shared.Tests
|
||||
|
||||
@project {
|
||||
type = "test"
|
||||
own-dep-projects = [
|
||||
"AyCode.Entities, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core, Mango.Nop.Services (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
MSTest integration and serialization tests. Covers SignalR client operations, JSON reference handling, binary serialization, Toon format, and bunit component rendering.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`TestData/`](TestData/README.md) | Test models for Toon serialization |
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MSTestSettings.cs`** — Parallel test execution at MethodLevel.
|
||||
- **`FruitBankClientTests.cs`** — (~667 lines) Full SignalR integration: Partner, Shipping, ShippingItem, ShippingDocument, Customer, Product, Order, Login tests. Localhost-only safety check.
|
||||
- **`OrderClientTests.cs`** — Order and StockTaking retrieval/manipulation tests.
|
||||
- **`JsonExtensionTests.cs`** — (~715 lines) JSON $id/$ref reference handling, 5-level hierarchies, circular references, DeepPopulateWithMerge.
|
||||
- **`StockTakingSerializerTests.cs`** — Binary serialization round-trips, null collection handling, binary format analysis.
|
||||
- **`ToonTests.cs`** — (~465 lines) Toon format: metadata generation, reference markers, type uniqueness, navigation metadata, property descriptions.
|
||||
- **`SandboxEndpointSimpleTests.cs`** — Endpoint connectivity and SignalR negotiate tests.
|
||||
- **`GridPartnerBaseTests.cs`** — Grid component tests (disabled).
|
||||
- **`GridPartnerRazorTests.cs`** — bunit Blazor rendering tests (disabled).
|
||||
|
|
@ -15,8 +15,8 @@ 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
|
||||
/// FONTOS: A SANDBOX-ot manu<EFBFBD>lisan kell elind<6E>tani a tesztek futtat<61>sa el<65>tt!
|
||||
/// Ind<EFBFBD>t<EFBFBD>s: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SandboxEndpointSimpleTests
|
||||
|
|
@ -24,7 +24,7 @@ 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)
|
||||
// Teszt SignalR Tags (TestSignalRTags-b<EFBFBD>l)
|
||||
private const int PingTag = SignalRTags.PingTag;
|
||||
private const int EchoTag = SignalRTags.EchoTag;
|
||||
private const int GetTestItemsTag = 9003;
|
||||
|
|
@ -34,13 +34,9 @@ public class SandboxEndpointSimpleTests
|
|||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTEL<EFBFBD>NK!");
|
||||
|
||||
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||
{
|
||||
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(SandboxEndpointSimpleTests))
|
||||
});
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(SandboxEndpointSimpleTests));
|
||||
}
|
||||
|
||||
#region HTTP Endpoint Tests
|
||||
|
|
@ -121,7 +117,7 @@ public class SandboxEndpointSimpleTests
|
|||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // Ellenőrizzük, hogy van Message property
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>k, hogy van Message property
|
||||
// Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) ||
|
||||
// root.TryGetProperty("message", out messageElement),
|
||||
// "Response should contain 'Message' property");
|
||||
|
|
@ -141,13 +137,13 @@ public class SandboxEndpointSimpleTests
|
|||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // Ellenőrizzük az Id-t
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>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
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>k a Name-et
|
||||
// Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) ||
|
||||
// root.TryGetProperty("name", out nameElement),
|
||||
// "Response should contain 'Name' property");
|
||||
|
|
@ -167,13 +163,13 @@ public class SandboxEndpointSimpleTests
|
|||
// using var jsonDoc = JsonDocument.Parse(response);
|
||||
// var root = jsonDoc.RootElement;
|
||||
|
||||
// // Ellenőrizzük, hogy tömb-e
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>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
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>k az els<6C> elemet
|
||||
// var firstItem = root[0];
|
||||
// Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _),
|
||||
// "Item should have 'Id' property");
|
||||
|
|
@ -187,8 +183,8 @@ public class SandboxEndpointSimpleTests
|
|||
//#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 ===
|
||||
//// === Az al<EFBFBD>bbi tesztek az eredeti 3 endpoint-ot tesztelik ===
|
||||
//// === Vissza<EFBFBD>ll<EFBFBD>t<EFBFBD>shoz: t<>r<EFBFBD>ld a kommenteket <20>s regisztr<74>ld az endpoint-okat a Program.cs-ben ===
|
||||
//// ===========================================
|
||||
|
||||
//// [TestMethod]
|
||||
|
|
@ -260,13 +256,13 @@ public class SandboxEndpointSimpleTests
|
|||
// 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!)
|
||||
// // K<EFBFBD>sz<EFBFBD>ts<EFBFBD>k el a request data-t
|
||||
// // Ha nincs param<EFBFBD>ter, null-t k<>ld<6C>nk (nem <20>res byte t<>mb<6D>t!)
|
||||
// byte[]? requestData = parameter != null
|
||||
// ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
|
||||
// : null;
|
||||
|
||||
// // A Hub metódus neve: OnReceiveMessage (3 paraméter: messageTag, messageBytes, requestId)
|
||||
// // A Hub met<EFBFBD>dus neve: OnReceiveMessage (3 param<61>ter: messageTag, messageBytes, requestId)
|
||||
// await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
|
||||
|
||||
// var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
|
||||
|
|
@ -276,7 +272,7 @@ public class SandboxEndpointSimpleTests
|
|||
// 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)
|
||||
// // Ellen<EFBFBD>rizz<EFBFBD>k, hogy valid JSON-e (ha van adat)
|
||||
// if (!string.IsNullOrEmpty(receivedJson))
|
||||
// {
|
||||
// try
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
# TestData
|
||||
|
||||
Demo models for Toon serialization testing.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`ToonTestData.cs`** — TestOrder, TestCustomer, TestOrderItem, TestProduct models with one-to-many relationships and back-references for testing complex object graph serialization.
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace FruitBankHybrid.Shared.Tests.TestData;
|
||||
|
||||
// Demo entity-k a teszteléshez
|
||||
public class TestOrder
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CustomerId { get; set; }
|
||||
public TestCustomer? Customer { get; set; }
|
||||
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestCustomer
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public List<TestOrder> Orders { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TestOrderItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int OrderId { get; set; }
|
||||
public TestOrder? Order { get; set; }
|
||||
public int ProductId { get; set; }
|
||||
public TestProduct? Product { get; set; }
|
||||
}
|
||||
|
||||
public class TestProduct
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Services.SignalRs;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Microsoft.AspNetCore.Http.Connections;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Test-only factory for <see cref="FruitBankSignalRClient"/>. Builds a <c>HubConnectionBuilder</c>
|
||||
/// with the same connection settings a production <c>Program.cs</c> would use, wires a logger factory
|
||||
/// backed by a single <c>SignaRClientLogItemWriter</c> (test-unit AppType, Detail level),
|
||||
/// and uses <see cref="BinaryProtocolMode.AsyncSegment"/> for the protocol.
|
||||
/// </summary>
|
||||
internal static class TestSignalRClientFactory
|
||||
{
|
||||
public static FruitBankSignalRClient Create(string testCategoryName)
|
||||
{
|
||||
var logWriters = new List<IAcLogWriterClientBase>
|
||||
{
|
||||
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName)
|
||||
};
|
||||
|
||||
Func<string, LoggerClient> loggerFactory =
|
||||
categoryName => new LoggerClient(categoryName, logWriters.ToArray());
|
||||
|
||||
var connectionOptions = new AcHubConnectionOptions
|
||||
{
|
||||
Url = $"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}",
|
||||
TransportMaxBufferSize = 30_000_000,
|
||||
ApplicationMaxBufferSize = 30_000_000,
|
||||
CloseTimeout = TimeSpan.FromSeconds(10),
|
||||
KeepAliveInterval = TimeSpan.FromSeconds(60),
|
||||
ServerTimeout = TimeSpan.FromSeconds(180),
|
||||
SkipNegotiation = true,
|
||||
Transports = HttpTransportType.WebSockets,
|
||||
UseAutomaticReconnect = true,
|
||||
UseStatefulReconnect = true
|
||||
};
|
||||
|
||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||
|
||||
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOptions);
|
||||
hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment);
|
||||
|
||||
return new FruitBankSignalRClient(hubBuilder, loggerFactory);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,461 @@
|
|||
using AyCode.Core.Enums;
|
||||
using AyCode.Core.Extensions;
|
||||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Core.Serializers.Jsons;
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Dtos;
|
||||
using FruitBank.Common.Entities;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using Mango.Nop.Core.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
using Nop.Core.Domain.Payments;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime.InteropServices.JavaScript;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Tests;
|
||||
|
||||
//1. "Headered List" (A biztonságos táblázatosítás)
|
||||
|
||||
//Az LLM-eknek nem kell minden sorban megismételni a mezőneveket, ha a lista elején egyszer definiálod a sorrendet.Ez nem találgatás, hanem egy lokális "szerződés".
|
||||
|
||||
//Hagyományos (pazarló):
|
||||
//Kódrészlet
|
||||
|
||||
//OrderItemDtos = [
|
||||
// OrderItemDto { Id = 120, Quantity = 10, ProductName = "Áfonya" }
|
||||
//OrderItemDto { Id = 121, Quantity = 5, ProductName = "Narancs" }
|
||||
//]
|
||||
|
||||
//Optimalizált(pontos és tömör) :
|
||||
//Kódrészlet
|
||||
|
||||
//OrderItemDtos: OrderItemDto[] = [
|
||||
// [Id, Quantity, ProductName]
|
||||
// [120, 10, "Áfonya"]
|
||||
// [121, 5, "Narancs"]
|
||||
//]
|
||||
|
||||
// Miért jó ez? Az LLM a fejléc alapján(mint egy CSV-nél) rendeli hozzá az értékeket a típushoz.Mivel a típus (OrderItemDto) ott van a definícióban, a szemantikai kapcsolat nem vész el.
|
||||
|
||||
//2. Típus-öröklődés a listákban
|
||||
|
||||
//Ha a @types részben már leírtad, hogy az OrderItemDto.ProductDto mezője egy ProductDto típust vár, akkor a @data részben felesleges kiírni a típusnevet minden egyes elemnél.
|
||||
|
||||
//Példa:
|
||||
//Kódrészlet
|
||||
|
||||
//// A 'ProductDto' elhagyható az objektum elől, mert a sémából tudja
|
||||
//ProductDto = {
|
||||
// Id = 1
|
||||
// Name = "Áfonya..."
|
||||
// GenericAttributes = [
|
||||
// { Id = 99, Key = "NetWeight", Value = "178.3" }
|
||||
// { Id = 100, Key = "GrossWeight", Value = "19" }
|
||||
// ]
|
||||
//}
|
||||
|
||||
//3. Alapértelmezett értékek elhagyása(Implicit Defaults)
|
||||
|
||||
//Ha egy mező értéke megegyezik a @types - ban definiált default-value-val, vagy null/0/false, akkor azt teljesen hagyd ki a @data részből.
|
||||
|
||||
// Szabály: Ami nincs ott, az az alapértelmezett.
|
||||
|
||||
// Token megtakarítás: A FruitBank példádban a GenericAttributes = < GenericAttributeDto[] > (count: 0)[] sorok rengeteg helyet foglalnak. Ha üres, egyszerűen ne küldd el a mezőt.
|
||||
|
||||
//4. String Table helyett: "Object Anchoring"
|
||||
|
||||
//használd az objektum-referenciákat (amit a @ProductDto:1 jelöléssel már el is kezdett a rendszered).
|
||||
|
||||
//Ha ugyanaz a Product szerepel 5 különböző rendelési tételnél, ne írd le ötször.
|
||||
|
||||
// Első alkalommal: ProductDto { ... }
|
||||
|
||||
//Minden további alkalommal: ProductDto = @ProductDto:1
|
||||
|
||||
//[ToonIgnore][ToonDataIgnore]
|
||||
[ToonDescription(Purpose = "Container model for Shipping, Order")]
|
||||
public class FullProcessModel
|
||||
{
|
||||
public List<Shipping> Shippings { get; set; }
|
||||
public List<OrderDto> Orders { get; set; }
|
||||
public List<StockTaking> StockTakings { get; set; }
|
||||
}
|
||||
|
||||
[TestClass]
|
||||
public sealed class ToonTests
|
||||
{
|
||||
private const int CustomerIdAasdDsserverCom = 6; //aasd@dsserver.com
|
||||
|
||||
private FruitBankSignalRClient _signalRClient = null!;
|
||||
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||
|
||||
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
[TestMethod]
|
||||
public async Task GetAnalyzeStringInternCandidatesLog()
|
||||
{
|
||||
var orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
|
||||
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||
//options.SetReferenceHandlingUnsafe(ReferenceHandlingMode.OnlyId);
|
||||
var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(orders, options);
|
||||
|
||||
Assert.IsNotNull(analysisLog);
|
||||
Assert.IsGreaterThan(0, analysisLog.Length);
|
||||
|
||||
// Print results sorted by occurrence count
|
||||
Console.WriteLine(analysisLog.ToString());
|
||||
Console.WriteLine();
|
||||
}
|
||||
#endif
|
||||
|
||||
[TestMethod]
|
||||
public async Task OrderDtoToToon()
|
||||
{
|
||||
var a = new FullProcessModel();
|
||||
a.Orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
a.Shippings = (await _signalRClient.GetShippings())!.Where(x=>x.Created > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||
|
||||
var toon = AcToonSerializer.Serialize(a, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
//var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
// Note: @ref: only appears when the same object instance is referenced multiple times.
|
||||
// Data from separate API calls typically don't share object instances.
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetMetaInfos()
|
||||
{
|
||||
var a = new FullProcessModel();
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||
Console.WriteLine(toon);
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public void ReferenceHandling_WithSharedReferences_ShouldOutputRefMarkers()
|
||||
{
|
||||
// Create a simple test container with shared references
|
||||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
|
||||
// Create a container that references the same ProductDto twice
|
||||
var container = new TestContainerWithSharedRefs
|
||||
{
|
||||
Product1 = sharedProduct,
|
||||
Product2 = sharedProduct // Same instance, should create @ref
|
||||
};
|
||||
|
||||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReferenceHandling_WithSharedIIdReferences_ShouldOutputRefMarkers()
|
||||
{
|
||||
// Create a simple test container with shared references
|
||||
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
var sharedProduct2 = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||
|
||||
// Create a container that references the same ProductDto twice
|
||||
var container = new TestContainerWithSharedRefs
|
||||
{
|
||||
Product1 = sharedProduct,
|
||||
Product2 = sharedProduct2 // Same instance, should create @ref
|
||||
};
|
||||
|
||||
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||
|
||||
Console.WriteLine(toon);
|
||||
Assert.IsNotEmpty(toon);
|
||||
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_ShouldNotContainList1OrGenericTypeNames()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
StringAssert.DoesNotMatch(toon, new System.Text.RegularExpressions.Regex(@"List`?1"), "A @meta.types vagy @types szekcióban nem szerepelhet List`1 vagy generikus típusnév.");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_ShouldNotContainDuplicateTypeNames()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var typesLine = toon.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("types = ["));
|
||||
Assert.IsNotNull(typesLine, "Nem található types lista a @meta szekcióban.");
|
||||
var typeNames = typesLine.Substring(typesLine.IndexOf('[') + 1, typesLine.LastIndexOf(']') - typesLine.IndexOf('[') - 1)
|
||||
.Split(',').Select(x => x.Trim(' ', '"')).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
|
||||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
Assert.IsTrue(duplicates.Count == 0, $"A types listában duplikált típusnév található: {string.Join(", ", duplicates)}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_EachTypeShouldBeDefinedOnceInTypesSection()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var typeDefLines = toon.Split('\n').Where(x => x.TrimEnd().EndsWith(": \"Object of type") || x.TrimEnd().EndsWith(": enum") || x.TrimEnd().EndsWith(": \"Object of type "));
|
||||
var typeNames = typeDefLines.Select(x => x.Trim().Split(':')[0]).ToList();
|
||||
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||
Assert.IsTrue(duplicates.Count == 0, $"A @types szekcióban duplikált típusdefiníció található: {string.Join(", ", duplicates)}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_PropertyTypesShouldNotReferenceList1()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
var lines = toon.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Trim().EndsWith(": List`1"))
|
||||
{
|
||||
Assert.Fail($"Property List`1 típusra hivatkozik: {line.Trim()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_PropertyDescriptions_ShouldNotBeRedundantOrMisleading()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>(FruitBankConstClient.DomainDescription);
|
||||
var lines = toon.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Trim().StartsWith("description:") && line.Contains("Collection of Object for"))
|
||||
{
|
||||
Assert.Fail($"Redundáns vagy félrevezető description: {line.Trim()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ToonTypes_NavigationMetadata_ShouldBeComplete()
|
||||
{
|
||||
var toon = AcToonSerializer.SerializeMetadata(FruitBankConstClient.DomainDescription, [typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]);
|
||||
Console.WriteLine(toon);
|
||||
Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n");
|
||||
|
||||
var lines = toon.Split('\n').Select(x => x.TrimEnd()).ToList();
|
||||
|
||||
// Ismerten egyirányú kapcsolatok - ezeknél nincs inverse property a másik oldalon
|
||||
// Customer: NopCommerce domain entity, nincs benne Orders kollekció
|
||||
// OrderNotes: OrderNote osztályban nincs Order navigation property
|
||||
// ProductDto: nincs benne OrderItems kollekció
|
||||
// GenericAttributes: polimorf kapcsolat ExpressionPredicate-tel, nincs inverse ÉS nincs egyértelmű FK
|
||||
// FONTOS: Csak az inverse-property hiányát engedjük! Az other-key-nek léteznie kell!
|
||||
var knownUnidirectionalNavigations = new HashSet<string>
|
||||
{
|
||||
"Customer",
|
||||
"OrderNotes",
|
||||
"ProductDto",
|
||||
"GenericAttributes",
|
||||
"ShippingDocumentFile",
|
||||
"Pallet"
|
||||
};
|
||||
|
||||
// GenericAttributes speciális eset - polimorf, nincs other-key sem
|
||||
var knownPolymorphicNavigations = new HashSet<string>
|
||||
{
|
||||
"GenericAttributes"
|
||||
};
|
||||
|
||||
// FK property-k NEM tartalmazhatnak foreign-key attribútumot
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (line.EndsWith("Id: int") || line.EndsWith("Id: int?"))
|
||||
{
|
||||
int j = i + 1;
|
||||
while (j < lines.Count)
|
||||
{
|
||||
if (lines[j].StartsWith(" ") && !lines[j].StartsWith(" "))
|
||||
break;
|
||||
|
||||
if (lines[j].StartsWith(" "))
|
||||
{
|
||||
var metaLine = lines[j].Trim();
|
||||
if (metaLine.StartsWith("foreign-key:"))
|
||||
{
|
||||
Assert.Fail($"FK property nem tartalmazhat foreign-key attribútumot: {line} -> {metaLine}");
|
||||
}
|
||||
}
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ FK property-k nem tartalmaznak foreign-key attribútumot\n");
|
||||
|
||||
// Számoljuk meg a hiányzó navigation metadatokat
|
||||
var missingMetadata = new List<string>();
|
||||
var skippedUnidirectional = new List<string>();
|
||||
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
|
||||
// Navigation property-k keresése
|
||||
if (line.Contains(": ") &&
|
||||
!line.Contains(": int") && !line.Contains(": string") &&
|
||||
!line.Contains(": DateTime") && !line.Contains(": decimal") &&
|
||||
!line.Contains(": bool") && !line.Contains(": Guid") &&
|
||||
!line.Contains(": double") && !line.Contains(": float") &&
|
||||
!line.Contains("description:") && !line.Contains("purpose:") &&
|
||||
!line.Contains("navigation:") && !line.Contains("foreign-key:") &&
|
||||
!line.Contains("table-name:") && !line.Contains("constraints:") &&
|
||||
!line.Contains("inverse-property:") && !line.Contains("other-key:") &&
|
||||
!line.Contains("primary-key:") && !line.Contains("examples:") &&
|
||||
!line.Contains("@meta") && !line.Contains("@types") &&
|
||||
!line.Contains("version") && !line.Contains("format") && !line.Contains("source-code-language") &&
|
||||
!line.Contains("underlying-type:") && !line.Contains("default-value:") && !line.Contains("values:"))
|
||||
{
|
||||
var propName = line.Split(':')[0].Trim();
|
||||
if (string.IsNullOrEmpty(propName) || propName == "types") continue;
|
||||
|
||||
// Következő sorok metadatainak összegyűjtése
|
||||
var metadata = new HashSet<string>();
|
||||
int j = i + 1;
|
||||
while (j < lines.Count && lines[j].StartsWith(" ") && lines[j].Trim().Contains(':'))
|
||||
{
|
||||
var metaLine = lines[j].Trim();
|
||||
if (metaLine.StartsWith("navigation:")) metadata.Add("navigation");
|
||||
if (metaLine.StartsWith("foreign-key:")) metadata.Add("foreign-key");
|
||||
if (metaLine.StartsWith("inverse-property:")) metadata.Add("inverse-property");
|
||||
if (metaLine.StartsWith("other-key:")) metadata.Add("other-key");
|
||||
j++;
|
||||
}
|
||||
|
||||
// Ha van navigation attribútum, ellenőrizzük a szükséges metadatokat
|
||||
if (metadata.Contains("navigation"))
|
||||
{
|
||||
var navLine = lines.Skip(i + 1).FirstOrDefault(x => x.Trim().StartsWith("navigation:"));
|
||||
if (navLine != null)
|
||||
{
|
||||
var isUnidirectional = knownUnidirectionalNavigations.Contains(propName);
|
||||
var isPolymorphic = knownPolymorphicNavigations.Contains(propName);
|
||||
|
||||
if (navLine.Contains("many-to-one"))
|
||||
{
|
||||
if (!metadata.Contains("foreign-key"))
|
||||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik foreign-key");
|
||||
|
||||
if (!metadata.Contains("inverse-property"))
|
||||
{
|
||||
if (isUnidirectional)
|
||||
skippedUnidirectional.Add($"{propName} (ManyToOne): egyirányú kapcsolat, nincs inverse");
|
||||
else
|
||||
missingMetadata.Add($"{propName} (ManyToOne): hiányzik inverse-property");
|
||||
}
|
||||
}
|
||||
else if (navLine.Contains("one-to-many"))
|
||||
{
|
||||
// other-key: polimorf kapcsolatoknál nem kötelező
|
||||
if (!metadata.Contains("other-key"))
|
||||
{
|
||||
if (isPolymorphic)
|
||||
{
|
||||
skippedUnidirectional.Add($"{propName} (OneToMany): polimorf kapcsolat, nincs other-key");
|
||||
}
|
||||
else
|
||||
{
|
||||
// DEBUG: részletes info
|
||||
Console.WriteLine($"\n[DEBUG] {propName} (OneToMany) - other-key hiányzik!");
|
||||
|
||||
// Keressük meg a property típusát a Toon outputban
|
||||
var propTypePart = line.Split(':').LastOrDefault()?.Trim() ?? "";
|
||||
Console.WriteLine($" Property type: {propTypePart}");
|
||||
|
||||
// Ha List<X> formátum, keressük meg X-et
|
||||
if (propTypePart.StartsWith("List<") && propTypePart.EndsWith(">"))
|
||||
{
|
||||
var elementTypeName = propTypePart.Substring(5, propTypePart.Length - 6);
|
||||
Console.WriteLine($" Element type: {elementTypeName}");
|
||||
|
||||
// Keressük meg az element type definícióját
|
||||
var elementTypeDefIndex = lines.FindIndex(l => l.Trim().StartsWith($"{elementTypeName}:"));
|
||||
if (elementTypeDefIndex >= 0)
|
||||
{
|
||||
Console.WriteLine($" Element type definition found at line {elementTypeDefIndex}");
|
||||
// Listázzuk ki az element type property-jeit amik "Id"-re végződnek
|
||||
for (int k = elementTypeDefIndex + 1; k < lines.Count; k++)
|
||||
{
|
||||
var propLine = lines[k];
|
||||
if (!propLine.StartsWith(" ")) break; // Új típus definíció
|
||||
if (propLine.StartsWith(" ")) continue; // Metaadat, skip
|
||||
|
||||
var trimmed = propLine.Trim();
|
||||
if (trimmed.EndsWith(": int") && trimmed.Contains("Id"))
|
||||
{
|
||||
Console.WriteLine($" FK candidate: {trimmed}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
missingMetadata.Add($"{propName} (OneToMany): hiányzik other-key");
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.Contains("inverse-property"))
|
||||
{
|
||||
if (isUnidirectional)
|
||||
skippedUnidirectional.Add($"{propName} (OneToMany): egyirányú kapcsolat, nincs inverse");
|
||||
else
|
||||
missingMetadata.Add($"{propName} (OneToMany): hiányzik inverse-property");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedUnidirectional.Count > 0)
|
||||
{
|
||||
Console.WriteLine("EGYIRÁNYÚ/POLIMORF KAPCSOLATOK (nem hiba):");
|
||||
foreach (var skipped in skippedUnidirectional)
|
||||
{
|
||||
Console.WriteLine($" ℹ {skipped}");
|
||||
}
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
if (missingMetadata.Count > 0)
|
||||
{
|
||||
Console.WriteLine("HIÁNYZÓ METAADATOK:");
|
||||
foreach (var missing in missingMetadata)
|
||||
{
|
||||
Console.WriteLine($" - {missing}");
|
||||
}
|
||||
Assert.Fail($"Hiányzó navigation metaadatok: {missingMetadata.Count} db");
|
||||
}
|
||||
|
||||
Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test helper class to verify reference handling with shared object instances.
|
||||
/// </summary>
|
||||
public class TestContainerWithSharedRefs
|
||||
{
|
||||
public ProductDto? Product1 { get; set; }
|
||||
public ProductDto? Product2 { get; set; }
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -7,6 +7,7 @@ using FruitBank.Common.Interfaces;
|
|||
using FruitBank.Common.SignalRs;
|
||||
using FruitBankHybrid.Shared.Pages;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Core.Domain.Common;
|
||||
using Nop.Core.Domain.Orders;
|
||||
|
||||
|
|
@ -51,7 +52,7 @@ public class GridGenericAttributeBase: FruitBankGridBase<GenericAttributeDto>, I
|
|||
switch (ParentDataItem)
|
||||
{
|
||||
case IProductDto:
|
||||
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = "Product";
|
||||
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = nameof(Product);
|
||||
|
||||
break;
|
||||
case IOrderDto:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
# Grids
|
||||
|
||||
Domain-specific grid components, one per entity type. All inherit `FruitBankGridBase<TEntity>`.
|
||||
|
||||
> For the MgGrid framework reference see: `AyCode.Blazor/AyCode.Blazor.Components/docs/MGGRID/README.md`
|
||||
|
||||
## FruitBankGridBase
|
||||
|
||||
`FruitBankGridBase<TDataItem>` is the project-specific adapter that fixes the generic parameters:
|
||||
|
||||
```
|
||||
MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient>
|
||||
```
|
||||
|
||||
Adds these defaults in `OnParametersSet` (based on `IsMasterGrid`):
|
||||
|
||||
| Setting | Master | Detail |
|
||||
|---|---|---|
|
||||
| `SizeMode` | `Small` | `Small` |
|
||||
| `ShowGroupPanel` | `true` | `false` |
|
||||
| `ShowSearchBox` | `true` | `false` |
|
||||
| `ShowFilterRow` | `true` | `false` |
|
||||
| `FilterMenuButtonDisplayMode` | `Never` | `Always` |
|
||||
| `DetailRowDisplayMode` | `Auto` | `Never` |
|
||||
| `DetailExpandButtonDisplayMode` | `Auto` | `Never` |
|
||||
| `PagerVisible` | `true` | `true` |
|
||||
| `PageSize` | 20 (Small) / 15 | 10 |
|
||||
| `AllowColumnReorder` | `true` | `true` |
|
||||
| `AllowGroup` | `true` | `false` |
|
||||
| `EditMode` | `EditRow` | `EditRow` |
|
||||
| `FocusedRowEnabled` | `true` | `true` |
|
||||
| `ColumnResizeMode` | `NextColumn` | `NextColumn` |
|
||||
| `PageSizeSelectorVisible` | `true` | `true` |
|
||||
|
||||
Also adds `OnCustomizeElement`: alternating row colors (`.alt-item`), header background (`#E6E6E6`), `hideDetailButton` for non-admin users.
|
||||
|
||||
## Legacy MgGridBase
|
||||
|
||||
`Components/MgGridBase.cs` — a non-generic legacy class that directly extends `DxGrid` and implements `IMgGridBase`. Used by older pages that predate the generic `MgGridBase<…>`. New grids should use `FruitBankGridBase<TEntity>` instead.
|
||||
|
||||
## Subfolders
|
||||
|
||||
| Folder | Entity | Notes |
|
||||
|---|---|---|
|
||||
| `GenericAttributes/` | `GridGenericAttributeBase` | Context-based (ContextIds: EntityId, KeyGroup, StoreId). Parent type switching: Product, Order, OrderItem |
|
||||
| `OrderItems/` | `GridOrderItem` | Commented out — placeholder |
|
||||
| `Partners/` | `GridPartnerBase` | Simple master grid with CRUD tags |
|
||||
| `Products/` | `GridStockQuantityHistoryDtoBase` | Detail grid under ProductDto |
|
||||
| `ShippingDocuments/` | `GridShippingDocumentBase` | Parent type switching: Shipping, Product, Partner. Sets ContextIds/KeyFieldNameToParentId per parent type |
|
||||
| `ShippingItems/` | `GridShippingItemBase` | Parent type switching: ShippingDocument, Shipping, Partner |
|
||||
| `Shippings/` | `GridShippingBase` | Simple master grid with CRUD tags |
|
||||
| `StockTakingItems/` | `GridStockTakingItemBase` | Simple master grid, GetAll only |
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Components
|
||||
|
||||
DevExpress Blazor grid wrappers, pallet measurement components, and toast notifications.
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`MgGridBase.cs`** — Legacy non-generic grid base (directly extends `DxGrid`). Used by older pages. New grids should use `FruitBankGridBase<TEntity>` — see [`Grids/README.md`](Grids/README.md).
|
||||
- **`GridProductDto.cs`** — Product data grid component.
|
||||
- **`OrderNotificationToast.razor`** — Toast notification for order updates.
|
||||
- **Pallet components** — PalletItemComponent.razor, GridShippingItemPallets.razor, GridDetailOrderItemPallets.razor.
|
||||
|
||||
## Subfolders
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Grids/`](Grids/README.md) | Domain-specific grid components by entity type |
|
||||
| [`FileUploads/`](FileUploads/README.md) | File upload components |
|
||||
| [`StockTakings/`](StockTakings/README.md) | Stock taking UI components |
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Databases
|
||||
|
||||
Client-side in-memory table cache using ConcurrentDictionary for offline/fast data access.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DatabaseClient.cs`** — (~250 lines) Local client-side database with typed tables (Shipping, ShippingDocument, ShippingItem, etc.). ProductDtoTable and OrderDtoTable with semaphore-based async loading. DatabaseTableBase<T> generic base. ObjectLock for thread-safe type-based locking. LoadingPanelVisibility global flag.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Extensions
|
||||
|
||||
DevExpress dialog helper extensions.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`DevexpressComponentExtensions.cs`** — ShowMessageBoxAsync() and ShowConfirmBoxAsync() for DevExpress dialogs.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
|
|
@ -72,4 +72,21 @@
|
|||
<ItemGroup>
|
||||
<Folder Include="Components\Toolbars\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- appsettings.json is the canonical config source for Web / Web.Client / MAUI hosts.
|
||||
They each pull it directly from disk (Web/Web.Client via <Target Copy>, MAUI via <EmbeddedResource>).
|
||||
Suppress the Razor SDK's auto-publish content behavior so the file does NOT flow into
|
||||
dependent projects' publish output — that would collide with each host's own copy
|
||||
(NETSDK1152 "multiple publish output files with the same relative path"). -->
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Layout
|
||||
|
||||
Application shell: root layout, navigation menu, auto-login, and toast notifications.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MainLayout.razor`** — Root layout with navigation menu.
|
||||
- **`MainLayout.razor.cs`** — SignalR message handling, auto-login on first render, toast notification for orders, login/logout handling, navigation guards.
|
||||
- **`NavMenu.razor`** — Navigation component.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Models
|
||||
|
||||
View models for measuring pages.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`MeasuringDateSelectorModel.cs`** — Date picker model: ShippingId, DateTime, IsMeasured flag.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Pages
|
||||
|
||||
Routed Blazor pages for the FruitBank application.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`Home.razor.cs`** — Landing page showing form factor and platform.
|
||||
- **`Login.razor.cs`** — Authentication with user selection and password validation.
|
||||
- **`ShippingsAdmin.razor.cs`** — Inbound shipping management with tabbed interface.
|
||||
- **`OrdersAdmin.razor.cs`** — Outbound order management with product and order item tabs.
|
||||
- **`MeasuringIn.razor.cs`** — Shipping measurement: calendar date picker, item detail, pallet recording.
|
||||
- **`MeasuringOut.razor.cs`** — Order measurement/audit: measurement tracking, approval workflow, RevisorId assignment.
|
||||
- **`StockTaking.razor.cs`** — Inventory management: stock taking sessions, item reconciliation.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# FruitBankHybrid.Shared
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Entities, AyCode.Interfaces, AyCode.Models, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
|
||||
"AyCode.Blazor.Components (in AyCode.Blazor repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Main Blazor UI library shared across all three deployment targets (Server, WASM, MAUI). Contains pages, DevExpress grid components, SignalR client, measurement service, and layout.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Components/`](Components/README.md) | DevExpress grid wrappers, pallet components, notifications |
|
||||
| [`Pages/`](Pages/README.md) | Routed pages: Login, ShippingsAdmin, OrdersAdmin, MeasuringIn/Out, StockTaking |
|
||||
| [`Services/`](Services/README.md) | SignalR client, measurement service, form factor, loggers |
|
||||
| [`Layout/`](Layout/README.md) | MainLayout with navigation, auto-login, toast notifications |
|
||||
| [`Models/`](Models/README.md) | Date selector model for measuring pages |
|
||||
| [`Extensions/`](Extensions/README.md) | DevExpress MessageBox/ConfirmBox helpers |
|
||||
| [`Databases/`](Databases/README.md) | Client-side ConcurrentDictionary table cache |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`_Imports.razor`** — Global Blazor imports.
|
||||
- **`Routes.razor`** — Route definitions.
|
||||
- **`appsettings.json`** — Canonical configuration source for all three hosts (Web, Web.Client, MAUI). Edit ONLY here. Pull mechanism per host: see `docs/ARCHITECTURE.md` (in repo root) → "Shared Configuration".
|
||||
|
||||
## Target Framework
|
||||
|
||||
.NET 10.0 with AOT compilation and WASM IL stripping enabled.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Loggers
|
||||
|
||||
Custom logger implementations for the FruitBank client.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`LoggerClient.cs`** — Non-generic and generic `LoggerClient<T>` extending AyCode logger base.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Services
|
||||
|
||||
Business logic, SignalR client, measurement helpers, and platform abstractions.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`IFormFactor.cs`** — Interface for device form factor detection.
|
||||
- **`IMeasurementService.cs`** — Measurement operation interface.
|
||||
- **`MeasurementService.cs`** — CSS styling for MeasuringStatus, pallet item creation/validation, status badge/text generation, shipping-level status calculation.
|
||||
|
||||
## Subfolders
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Loggers/`](Loggers/README.md) | LoggerClient and LoggerClient<T> extending AyCode logger |
|
||||
| [`SignalRs/`](SignalRs/README.md) | FruitBankSignalRClient hub client + DataSource wrappers |
|
||||
|
|
@ -19,20 +19,16 @@ using Microsoft.AspNetCore.SignalR.Client;
|
|||
using Nop.Core.Domain.Customers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ServiceModel.Channels;
|
||||
using AyCode.Core.Serializers;
|
||||
using Mango.Nop.Core.Entities;
|
||||
|
||||
namespace FruitBankHybrid.Shared.Services.SignalRs
|
||||
{
|
||||
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()))
|
||||
public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func<string, LoggerClient> loggerFactory)
|
||||
: base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient)))
|
||||
{
|
||||
//var hubConnection = new HubConnectionBuilder()
|
||||
// .WithUrl("fullHubName")
|
||||
// .WithAutomaticReconnect()
|
||||
// .WithStatefulReconnect()
|
||||
// .WithKeepAliveInterval(TimeSpan.FromSeconds(60))
|
||||
// .WithServerTimeout(TimeSpan.FromSeconds(120))
|
||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||
ConstHelper.NameByValue<SignalRTags>(0);
|
||||
}
|
||||
|
|
@ -42,10 +38,14 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
|
|||
/// </summary>
|
||||
public event Func<int, SignalResponseDataMessage?, Task> OnMessageReceived = null!;
|
||||
|
||||
protected override async Task MessageReceived(int messageTag, byte[] messageBytes)
|
||||
protected override async Task MessageReceived(int messageTag, SignalParams signalParams, object data)
|
||||
{
|
||||
var responseDataMessage = messageBytes.BinaryTo<SignalResponseDataMessage>();
|
||||
|
||||
var responseDataMessage = new SignalResponseDataMessage
|
||||
{
|
||||
Status = signalParams.Status,
|
||||
DataSerializerType = AcSerializerType.Binary,
|
||||
RawResponseData = data
|
||||
};
|
||||
await OnMessageReceived(messageTag, responseDataMessage);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# SignalRs
|
||||
|
||||
Main SignalR hub client and data source wrappers.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`FruitBankSignalRClient.cs`** — (~343 lines) Central hub client for ALL server communication. Methods for Partners, Shippings, ShippingItems, ShippingDocuments, Orders, OrderItems, OrderItemPallets, Products, StockTaking, GenericAttributes, Authentication.
|
||||
- **`SignalRDataSource.cs`** — `SignalRDataSourceList<T>` and `SignalRDataSourceObservable<T>` wrappers for DevExpress grid binding.
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AyCode": {
|
||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||
"Urls": {
|
||||
"BaseUrl": "https://localhost:59579",
|
||||
"ApiBaseUrl": "https://localhost:59579"
|
||||
},
|
||||
"Logger": {
|
||||
"AppType": "Server",
|
||||
"LogLevel": "Detail",
|
||||
"LogWriters": [
|
||||
{
|
||||
"LogLevel": "Detail",
|
||||
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
"AcHubConnection": {
|
||||
"Url": "https://localhost:59579/fbHub",
|
||||
"TransportMaxBufferSize": 30000000,
|
||||
"ApplicationMaxBufferSize": 30000000,
|
||||
"CloseTimeout": "00:00:10",
|
||||
"KeepAliveInterval": "00:01:00",
|
||||
"ServerTimeout": "00:03:00",
|
||||
"SkipNegotiation": true,
|
||||
"Transports": "WebSockets",
|
||||
"UseAutomaticReconnect": true,
|
||||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "DoubleBuffered",
|
||||
"FlushTimeout": "00:00:10"
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,27 @@
|
|||
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shared appsettings.json synced into wwwroot at build time.
|
||||
Approach: pre-build Copy Target (not <Content Include Link=...>), because:
|
||||
- <Content Include="..\..." Link="wwwroot\..."/> triggers StaticWebAssets.Normalize() "Illegal characters"
|
||||
- pre-normalizing via [System.IO.Path]::GetFullPath(...) still fails (the absolute path also trips the validator)
|
||||
- a pre-build Copy creates a physical wwwroot/appsettings.json which the StaticWebAssets auto-discovery
|
||||
picks up naturally, same as any other wwwroot file
|
||||
BeforeTargets lists multiple early targets to ensure the Copy runs before static-asset discovery,
|
||||
regardless of which one triggers first in a given SDK version.
|
||||
NOTE: a clean build (delete obj/) is required the first time, because the static-asset manifest
|
||||
is cached in obj/ and stale entries persist across incremental builds.
|
||||
The physical wwwroot/appsettings.json is a build artifact — commit or gitignore per team policy;
|
||||
edits should always be made in FruitBankHybrid.Shared/appsettings.json (the canonical source). -->
|
||||
<Target Name="CopySharedAppSettings"
|
||||
BeforeTargets="CollectPackageReferences;AssignTargetPaths;ResolveStaticWebAssetsInputs;BeforeBuild"
|
||||
Inputs="..\FruitBankHybrid.Shared\appsettings.json"
|
||||
Outputs="wwwroot\appsettings.json">
|
||||
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
|
||||
DestinationFiles="wwwroot\appsettings.json"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="AyCode.Core">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Services.SignalRs;
|
||||
using FruitBank.Common;
|
||||
using FruitBank.Common.Loggers;
|
||||
|
|
@ -6,11 +7,13 @@ using FruitBank.Common.Models;
|
|||
using FruitBank.Common.Services;
|
||||
using FruitBankHybrid.Shared.Databases;
|
||||
using FruitBankHybrid.Shared.Services;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using FruitBankHybrid.Web.Client.Services;
|
||||
using FruitBankHybrid.Web.Client.Services.Loggers;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
|
|
@ -20,21 +23,55 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres
|
|||
builder.Services.AddSingleton<IFormFactor, FormFactor>();
|
||||
builder.Services.AddSingleton<ISecureCredentialService, WebSecureCredentialService>();
|
||||
|
||||
//#if DEBUG
|
||||
#if DEBUG
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
|
||||
//#endif
|
||||
#endif
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
|
||||
// Bind SignalR options from wwwroot/appsettings.json (loaded automatically by WebAssemblyHostBuilder) —
|
||||
// single Configure call per options type, combining section Bind with runtime overrides.
|
||||
builder.Services.Configure<AcHubConnectionOptions>(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts));
|
||||
|
||||
builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
|
||||
{
|
||||
builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts);
|
||||
|
||||
// WASM safety net: AsyncSegment send-path is unsupported here — Validate() would throw.
|
||||
// Downgrade if appsettings.json accidentally specifies it.
|
||||
if (opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) opts.ProtocolMode = BinaryProtocolMode.Segment;
|
||||
});
|
||||
|
||||
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
|
||||
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
|
||||
|
||||
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
|
||||
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
|
||||
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
|
||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||
|
||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||
|
||||
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||
return hubBuilder;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<LoggedInModel>(sp =>
|
||||
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
builder.Services.AddSingleton<FruitBankSignalRClient>();
|
||||
builder.Services.AddSingleton<DatabaseClient>();
|
||||
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||
|
||||
#if DEBUG
|
||||
if (FruitBankConstClient.SignalRSerializerDiagnosticLog)
|
||||
{
|
||||
SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); };
|
||||
//SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); };
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# FruitBankHybrid.Web.Client
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Models, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
Blazor WebAssembly client running in the browser after server prerendering. .NET 10.0.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Services/`](Services/README.md) | WASM-specific: FormFactor, credential storage, console logging |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`Program.cs`** — WASM startup, DI registration.
|
||||
- **`_Imports.razor`** — Global imports.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Services
|
||||
|
||||
WASM-specific service implementations.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`FormFactor.cs`** — Returns "WebAssembly" as form factor.
|
||||
- **`WebSecureCredentialService.cs`** — localStorage-backed credential storage with XOR obfuscation + Base64 encoding (NOT cryptographically secure). 2-day expiration.
|
||||
|
||||
## Subfolders
|
||||
|
||||
- **`Loggers/BrowserConsoleLogWriter.cs`** — Browser console logging via JS interop.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,44 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AyCode": {
|
||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||
"Urls": {
|
||||
"BaseUrl": "https://localhost:59579",
|
||||
"ApiBaseUrl": "https://localhost:59579"
|
||||
},
|
||||
"Logger": {
|
||||
"AppType": "Server",
|
||||
"LogLevel": "Detail",
|
||||
"LogWriters": [
|
||||
{
|
||||
"LogLevel": "Detail",
|
||||
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
"AcHubConnection": {
|
||||
"Url": "https://localhost:59579/fbHub",
|
||||
"TransportMaxBufferSize": 30000000,
|
||||
"ApplicationMaxBufferSize": 30000000,
|
||||
"CloseTimeout": "00:00:10",
|
||||
"KeepAliveInterval": "00:01:00",
|
||||
"ServerTimeout": "00:03:00",
|
||||
"SkipNegotiation": true,
|
||||
"Transports": "WebSockets",
|
||||
"UseAutomaticReconnect": true,
|
||||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "DoubleBuffered",
|
||||
"FlushTimeout": "00:00:10"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# Components
|
||||
|
||||
Blazor Server app shell components.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`App.razor`** — Root component: DevExpress theme, asset configuration, render mode.
|
||||
- **`_Imports.razor`** — Global imports.
|
||||
- **`Pages/Error.razor`** — Error page with request ID tracking.
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<!--<PublishTrimmed>true</PublishTrimmed>-->
|
||||
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
<RunAOTCompilation>true</RunAOTCompilation>
|
||||
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
|
||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||
</PropertyGroup>
|
||||
|
|
@ -85,6 +85,19 @@
|
|||
<Folder Include="Services\SignalRs\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shared appsettings.json copied into project root at build time.
|
||||
Why a Copy target instead of <Content Include="..\..." Link="appsettings.json" CopyToOutputDirectory="PreserveNewest"/>:
|
||||
the Link approach only copies the file to bin/Debug output, but ASP.NET Core's WebApplicationBuilder
|
||||
in development reads appsettings.json from the ContentRoot (= project directory), not from the output folder.
|
||||
Materializing the file physically in the project root makes it discoverable by the default configuration
|
||||
loader in every run mode (F5 / dotnet run / published), and the SDK auto-include for appsettings*.json
|
||||
takes care of copy-to-output from there. -->
|
||||
<Target Name="CopySharedAppSettings" BeforeTargets="BeforeBuild" Inputs="..\FruitBankHybrid.Shared\appsettings.json" Outputs="appsettings.json">
|
||||
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
|
||||
DestinationFiles="appsettings.json"
|
||||
SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,14 +1,18 @@
|
|||
using AyCode.Core.Loggers;
|
||||
using AyCode.Core.Serializers.Binaries;
|
||||
using AyCode.Services.SignalRs;
|
||||
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;
|
||||
using FruitBankHybrid.Shared.Services;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
using FruitBankHybrid.Web.Components;
|
||||
using FruitBankHybrid.Web.Services;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -16,13 +20,49 @@ 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>(sp =>
|
||||
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
|
||||
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterBase.
|
||||
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||
builder.Services.AddAcLoggerFactory<LoggerClient>();
|
||||
|
||||
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
|
||||
// Bind SignalR options from appsettings.json — single Configure call per options type.
|
||||
// The lambda runs the appsettings Bind first, then any runtime overrides (e.g. the WASM safety net).
|
||||
builder.Services.Configure<AcHubConnectionOptions>(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts));
|
||||
|
||||
builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
|
||||
{
|
||||
builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts);
|
||||
|
||||
// Platform safety net: on WebAssembly the AsyncSegment send-path is unsupported
|
||||
// (Validate() would throw). No-op on this server host, but matches the contract.
|
||||
if (OperatingSystem.IsBrowser() && opts.ProtocolMode == BinaryProtocolMode.AsyncSegment)
|
||||
opts.ProtocolMode = BinaryProtocolMode.Segment;
|
||||
});
|
||||
|
||||
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
|
||||
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
|
||||
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
|
||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||
|
||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||
|
||||
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||
return hubBuilder;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<FruitBankSignalRClient>();
|
||||
builder.Services.AddSingleton<DatabaseClient>();
|
||||
|
|
@ -45,9 +85,6 @@ else
|
|||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.MapHub<LoggerSignalRHub>($"/{FruitBankConstClient.LoggerHubName}");
|
||||
app.MapHub<DevAdminSignalRHub>($"/{FruitBankConstClient.DefaultHubName}");
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
|
@ -57,8 +94,6 @@ app.MapStaticAssets();
|
|||
app.MapRazorComponents<App>()
|
||||
//.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(
|
||||
typeof(FruitBankHybrid.Shared._Imports).Assembly,
|
||||
typeof(FruitBankHybrid.Web.Client._Imports).Assembly);
|
||||
.AddAdditionalAssemblies(typeof(FruitBankHybrid.Shared._Imports).Assembly, typeof(FruitBankHybrid.Web.Client._Imports).Assembly);
|
||||
|
||||
app.Run();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# FruitBankHybrid.Web
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Models, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
ASP.NET Core Blazor Server host. Serves the web interface, hosts SignalR hubs, and supports interactive WebAssembly rendering.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Services/`](Services/README.md) | Server-side FormFactor, SecureCredentialService (no-op), SignalR hub setup |
|
||||
| [`Components/`](Components/README.md) | App.razor with DevExpress theme, Error page |
|
||||
| `Controllers/` | Empty placeholder |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`Program.cs`** — DI, SignalR hub mapping (DevAdminSignalRHub, LoggerSignalRHub), 256KB max message size, DevExpress Fluent theme, static asset versioning.
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Services
|
||||
|
||||
Server-side service implementations.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`FormFactor.cs`** — Returns "Web" as form factor.
|
||||
- **`ServerSecureCredentialService.cs`** — No-op implementation (clients handle credential storage).
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,44 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AyCode": {
|
||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||
"Urls": {
|
||||
"BaseUrl": "https://localhost:59579",
|
||||
"ApiBaseUrl": "https://localhost:59579"
|
||||
},
|
||||
"Logger": {
|
||||
"AppType": "Server",
|
||||
"LogLevel": "Detail",
|
||||
"LogWriters": [
|
||||
{
|
||||
"LogLevel": "Detail",
|
||||
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"AyCode": {
|
||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||
"Urls": {
|
||||
"BaseUrl": "https://localhost:7144",
|
||||
"ApiBaseUrl": "https://localhost:7144"
|
||||
},
|
||||
"Logger": {
|
||||
"AppType": "Server",
|
||||
"LogLevel": "Detail",
|
||||
"LogWriters": [
|
||||
{
|
||||
"LogLevel": "Detail",
|
||||
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"AcHubConnection": {
|
||||
"Url": "https://localhost:59579/fbHub",
|
||||
"TransportMaxBufferSize": 30000000,
|
||||
"ApplicationMaxBufferSize": 30000000,
|
||||
"CloseTimeout": "00:00:10",
|
||||
"KeepAliveInterval": "00:01:00",
|
||||
"ServerTimeout": "00:03:00",
|
||||
"SkipNegotiation": true,
|
||||
"Transports": "WebSockets",
|
||||
"UseAutomaticReconnect": true,
|
||||
"UseStatefulReconnect": true
|
||||
},
|
||||
"AcBinaryHubProtocol": {
|
||||
"ProtocolMode": "AsyncSegment",
|
||||
"BufferSize": 4096,
|
||||
"FlushPolicy": "DoubleBuffered",
|
||||
"FlushTimeout": "00:00:10"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11222.15
|
||||
|
|
@ -33,10 +33,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
ProjectSection(SolutionItems) = preProject
|
||||
..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt = ..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt
|
||||
SqlSchemaCompare_Dev_to_Prod.scmp = SqlSchemaCompare_Dev_to_Prod.scmp
|
||||
.github\copilot-instructions.md = .github\copilot-instructions.md
|
||||
CLAUDE.md = CLAUDE.md
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Blazor.Components.Tests", "..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components.Tests\AyCode.Blazor.Components.Tests.csproj", "{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B7D3E8A1-F4C2-4E9D-A6B5-1C3D5E7F9A2B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
docs\ARCHITECTURE.md = docs\ARCHITECTURE.md
|
||||
docs\CONVENTIONS.md = docs\CONVENTIONS.md
|
||||
docs\GLOSSARY.md = docs\GLOSSARY.md
|
||||
docs\SCHEMA.md = docs\SCHEMA.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core", "..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj", "{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.SourceGenerator", "..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj", "{1C882DAC-5027-BD65-9F22-A5FFF813FA36}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -101,6 +116,14 @@ Global
|
|||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0.2</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<PackageVersion>$(ApplicationDisplayVersion)</PackageVersion>
|
||||
|
||||
<RunAOTCompilation>false</RunAOTCompilation>
|
||||
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
|
||||
|
|
@ -78,6 +79,12 @@
|
|||
<AndroidKeyStore>True</AndroidKeyStore>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Shared appsettings.json linked from FruitBankHybrid.Shared, embedded so MauiAppBuilder can load via GetManifestResourceStream.
|
||||
LogicalName preserves the original manifest resource name so the loader in MauiProgram.cs doesn't need changes. -->
|
||||
<EmbeddedResource Include="..\FruitBankHybrid.Shared\appsettings.json" Link="appsettings.json" LogicalName="FruitBankHybrid.appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- App Icon -->
|
||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||
|
|
@ -131,11 +138,11 @@
|
|||
<Reference Include="AyCode.Entities">
|
||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.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>
|
||||
<Reference Include="Mango.Nop.Core">
|
||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Nop.Core">
|
||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Nop.Core.dll</HintPath>
|
||||
<!--<Reference Include="Nop.Core">
|
||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Nop.Core.dll</HintPath>
|
||||
</Reference>-->
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AyCode.Core.Loggers;
|
||||
using AyCode.Services.SignalRs;
|
||||
using FruitBank.Common.Loggers;
|
||||
using FruitBank.Common.Models;
|
||||
using FruitBank.Common.Services;
|
||||
|
|
@ -6,9 +7,15 @@ using FruitBankHybrid.Services;
|
|||
using FruitBankHybrid.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Databases;
|
||||
using FruitBankHybrid.Shared.Services;
|
||||
using FruitBankHybrid.Shared.Services.Loggers;
|
||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||
//using DevExpress.Maui;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Reflection;
|
||||
|
||||
namespace FruitBankHybrid
|
||||
{
|
||||
|
|
@ -27,22 +34,59 @@ namespace FruitBankHybrid
|
|||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
});
|
||||
|
||||
// Load embedded appsettings.json — MAUI has no automatic config file discovery,
|
||||
// so the JSON is shipped as an EmbeddedResource (see FruitBankHybrid.csproj).
|
||||
using (var appsettingsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("FruitBankHybrid.appsettings.json"))
|
||||
{
|
||||
if (appsettingsStream is not null)
|
||||
{
|
||||
var jsonConfig = new ConfigurationBuilder().AddJsonStream(appsettingsStream).Build();
|
||||
builder.Configuration.AddConfiguration(jsonConfig);
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
|
||||
#endif
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||
|
||||
// Logger options + framework factory. LoggerClient instances are created per caller category,
|
||||
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
|
||||
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
|
||||
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
|
||||
|
||||
// Bind SignalR options from configuration.
|
||||
// Precedence: code default → appsettings.json (this line) → any later Configure<T> action.
|
||||
builder.Services.Configure<AcHubConnectionOptions>(builder.Configuration.GetSection("AcHubConnection"));
|
||||
builder.Services.Configure<AcBinaryHubProtocolOptions>(builder.Configuration.GetSection("AcBinaryHubProtocol"));
|
||||
|
||||
// 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>(sp =>
|
||||
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
|
||||
|
||||
// SignalR HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
|
||||
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
|
||||
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
|
||||
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
|
||||
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
|
||||
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
|
||||
{
|
||||
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
|
||||
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
|
||||
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
|
||||
|
||||
var logger = loggerFactory(nameof(FruitBankSignalRClient));
|
||||
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
|
||||
|
||||
hubBuilder.AddAcBinaryProtocol(protocolOpts);
|
||||
return hubBuilder;
|
||||
});
|
||||
|
||||
builder.Services.AddSingleton<FruitBankSignalRClient>();
|
||||
builder.Services.AddSingleton<DatabaseClient>();
|
||||
|
||||
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
|
||||
|
||||
|
||||
builder.Services.AddMauiBlazorWebView();
|
||||
builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Platforms
|
||||
|
||||
Per-platform entry points for MAUI.
|
||||
|
||||
## Folders
|
||||
|
||||
- **`Android/`** — MainActivity.cs, MainApplication.cs (custom keystore config).
|
||||
- **`iOS/`** — AppDelegate.cs, Program.cs.
|
||||
- **`MacCatalyst/`** — AppDelegate.cs, Program.cs.
|
||||
- **`Windows/`** — App.xaml.cs (WinUI entry point).
|
||||
|
|
@ -16,7 +16,7 @@ namespace FruitBankHybrid.WinUI
|
|||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
//this.InitializeComponent();
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# FruitBankHybrid
|
||||
|
||||
@project {
|
||||
type = "product"
|
||||
own-dep-projects = [
|
||||
"AyCode.Core, AyCode.Services, AyCode.Entities (in AyCode.Core repo)",
|
||||
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
|
||||
]
|
||||
}
|
||||
|
||||
.NET MAUI Hybrid cross-platform app hosting Blazor components via BlazorWebView. Targets Android (API 33+), iOS (15.0+), and Windows.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|---|---|
|
||||
| [`Services/`](Services/README.md) | Platform-specific: FormFactor, SecureCredentialService, BrowserConsoleLogWriter |
|
||||
| [`Platforms/`](Platforms/README.md) | Per-platform entry points: Android, iOS, Windows |
|
||||
| `Components/` | Razor component imports (_Imports.razor) |
|
||||
| `Resources/` | AppIcon, splash screens, fonts, images |
|
||||
|
||||
## Key Files (Root)
|
||||
|
||||
- **`MauiProgram.cs`** — DI registration, DevExpress init, SignalR client setup.
|
||||
- **`MainPage.xaml.cs`** — BlazorWebView host page.
|
||||
- **`App.xaml.cs`** — MAUI Application entry point.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Services
|
||||
|
||||
MAUI platform-specific service implementations.
|
||||
|
||||
## Key Files
|
||||
|
||||
- **`FormFactor.cs`** — Device idiom detection (Phone, Tablet, Desktop).
|
||||
- **`MauiSecureCredentialService.cs`** — SecureStorage-backed credential persistence with 2-day expiration.
|
||||
|
||||
## Subfolders
|
||||
|
||||
- **`Loggers/BrowserConsoleLogWriter.cs`** — Browser console logging bridge for BlazorWebView.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# FruitBankHybridApp
|
||||
|
||||
nopCommerce plugin for FruitBank, a fruit & vegetable wholesaler. Manages supplier inbound delivery (Shipping), outgoing orders (Order), warehouse weighing, and inventory stocktaking. Runs as Blazor Server, Blazor WASM, and MAUI Hybrid (Android/iOS/Windows).
|
||||
|
||||
nopCommerce 4.80.9 requires it
|
||||
|
||||
## LLM Context
|
||||
|
||||
Domain rules and critical pitfalls live in a single file: [`.github/copilot-instructions.md`](.github/copilot-instructions.md)
|
||||
|
||||
| Tool | Auto-loaded | Action needed |
|
||||
|------|------------|---------------|
|
||||
| GitHub Copilot | ✅ `copilot-instructions.md` | None |
|
||||
| Claude Code | ✅ `CLAUDE.md` → references above | None |
|
||||
| Cursor / Windsurf | ✅ `README.md` | Read `copilot-instructions.md` via @file |
|
||||
|
||||
Detailed docs: `docs/` — GLOSSARY.md, ARCHITECTURE.md, CONVENTIONS.md. Domain model schema (TOON) lives in the plugin: `NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md`
|
||||
|
||||
## Solution Structure
|
||||
|
||||
| Project | TFM | Purpose | README |
|
||||
|---|---|---|---|
|
||||
| [`FruitBank.Common`](FruitBank.Common/README.md) | net9.0 | Shared domain: entities, DTOs, interfaces, SignalR tags, measurement helpers | [README](FruitBank.Common/README.md) |
|
||||
| [`FruitBank.Common.Server`](FruitBank.Common.Server/README.md) | net9.0 | Server-side: SignalR hubs, broadcast service, logging, nopCommerce integration | [README](FruitBank.Common.Server/README.md) |
|
||||
| [`FruitBankHybrid.Shared`](FruitBankHybrid.Shared/README.md) | net10.0 | Blazor UI: pages, grids, SignalR client, measurement service, layout | [README](FruitBankHybrid.Shared/README.md) |
|
||||
| [`FruitBankHybrid.Shared.Common`](FruitBankHybrid.Shared.Common/README.md) | net10.0 | Shared common library (placeholder) | [README](FruitBankHybrid.Shared.Common/README.md) |
|
||||
| [`FruitBankHybrid`](FruitBankHybrid/README.md) | net10.0‑android/ios/win | MAUI Hybrid app: Android, iOS, Windows | [README](FruitBankHybrid/README.md) |
|
||||
| [`FruitBankHybrid.Web`](FruitBankHybrid.Web/README.md) | net10.0 | Blazor Server host with SignalR hubs | [README](FruitBankHybrid.Web/README.md) |
|
||||
| [`FruitBankHybrid.Web.Client`](FruitBankHybrid.Web.Client/README.md) | net10.0 | Blazor WebAssembly client | [README](FruitBankHybrid.Web.Client/README.md) |
|
||||
|
||||
### Test Projects
|
||||
|
||||
| Project | TFM | Purpose | README |
|
||||
|---|---|---|---|
|
||||
| [`FruitBankHybrid.Shared.Tests`](FruitBankHybrid.Shared.Tests/README.md) | net10.0 | Integration + serialization tests (SignalR, JSON, Toon, bunit) | [README](FruitBankHybrid.Shared.Tests/README.md) |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
All projects reference these via **DLL** (not ProjectReference). Full source is available in sibling directories:
|
||||
|
||||
| Repo | Path | Key Docs |
|
||||
|---|---|---|
|
||||
| **AyCode.Core** (net9.0) | `../../../Aycode/Source/AyCode.Core/` | [copilot-instructions](../../../Aycode/Source/AyCode.Core/.github/copilot-instructions.md), [ARCHITECTURE](../../../Aycode/Source/AyCode.Core/docs/ARCHITECTURE.md) |
|
||||
| **AyCode.Blazor** (net10.0) | `../../../Aycode/Source/AyCode.Blazor/` | [copilot-instructions](../../../Aycode/Source/AyCode.Blazor/.github/copilot-instructions.md), [MGGRID](../../../Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/docs/MGGRID/README.md) |
|
||||
| **Mango.Nop Libraries** (net9.0) | `../NopCommerce.Common/4.70/Libraries/` | [copilot-instructions](../NopCommerce.Common/4.70/Libraries/.github/copilot-instructions.md), [ARCHITECTURE](../NopCommerce.Common/4.70/Libraries/docs/ARCHITECTURE.md) |
|
||||
| **FruitBank Plugin** (net9.0) | `../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/` | [README](../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/README.md) |
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,44 @@
|
|||
# Conventions
|
||||
|
||||
> For core framework conventions see: `CONVENTIONS.md` (in AyCode.Core repo)
|
||||
> For UI framework conventions see: `CONVENTIONS.md` (in AyCode.Blazor repo)
|
||||
|
||||
## Naming
|
||||
|
||||
- **fb prefix** on database tables: `fbPallet`, `fbShipping`, `fbShippingItem`, etc.
|
||||
- **Dto suffix** for DTOs wrapping nopCommerce entities: `OrderDto`, `OrderItemDto`, `ProductDto`.
|
||||
- **XxxItemPallet** for measurement records: `ShippingItemPallet`, `OrderItemPallet`, `StockTakingItemPallet`.
|
||||
- **Grid prefix** for Blazor grid components: `GridPartnerBase`, `GridShippingBase`, etc.
|
||||
- **GridXxxBase** = C# code-behind class inheriting `FruitBankGridBase<TDataItem>`.
|
||||
- **GridXxx.razor** = Razor markup using `<GridXxxBase>` with `<Columns>` and `<DetailRowTemplate>`.
|
||||
- **OnGrid prefix** for MgGridBase event parameters: `OnGridItemDeleting`, `OnGridEditModelSaving`, `OnGridFocusedRowChanged`, etc. (avoids collision with DxGrid base events).
|
||||
- **SignalRTags** constants use numeric ranges by domain (see `FruitBank.Common/SignalRs/`).
|
||||
|
||||
## XML Documentation
|
||||
|
||||
`<summary>` — brief, developer-facing, readable in VS IntelliSense tooltip. NO implementation details, NO wire-format / byte-level / perf specifics — those live in `docs/TOPIC/*.md`. Add `<example>` only when usage is non-obvious; otherwise omit.
|
||||
|
||||
## Patterns
|
||||
|
||||
- **MeasuringItemPalletBase** as abstract base for all three measurement hierarchies.
|
||||
- **GenericAttributes** for extending nopCommerce entities with custom data (IsMeasurable, Tare, AverageWeight).
|
||||
- **Composition interfaces** for measurement traits: IMeasuringValues = IMeasuringWeights + IMeasuringQuantity.
|
||||
- **DevExpress DxGrid** with `AcSignalRDataSource` for real-time grid data.
|
||||
- **MgGridBase** — canonical grid base from AyCode.Blazor (see `AyCode.Blazor.Components/docs/MGGRID/README.md` (in AyCode.Blazor repo)). Provides SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen.
|
||||
- **FruitBankGridBase** — project adapter that fixes `TId=int`, `TLoggerClient=LoggerClient`, adds per-user layout and master/detail defaults.
|
||||
- **FruitBankSignalRClient** as single hub client for all server communication.
|
||||
- **DatabaseClient** for client-side caching with ConcurrentDictionary tables.
|
||||
|
||||
### Grid Creation Checklist
|
||||
|
||||
1. Create `GridXxxBase.cs` inheriting `FruitBankGridBase<TEntity>`.
|
||||
2. Set CRUD tags in constructor: `GetAllMessageTag`, `AddMessageTag`, `UpdateMessageTag`, `RemoveMessageTag`.
|
||||
3. Create `GridXxx.razor` with `<GridXxxBase>`, `<Columns>`, optional `<DetailRowTemplate>`.
|
||||
4. Wrap in `<MgGridWithInfoPanel>` if InfoPanel is needed.
|
||||
5. For detail grids: set `ParentDataItem`, `KeyFieldNameToParentId`, `ContextIds`.
|
||||
|
||||
## UI (Hungarian Locale)
|
||||
|
||||
- Status labels and UI text are in Hungarian.
|
||||
- MeasuringStatus display: "Nincs elkezdve", "Elkezdve", "Kész", "Auditált".
|
||||
- Date format follows Hungarian conventions.
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Glossary / Fogalomtár
|
||||
|
||||
> For core framework glossary see: `GLOSSARY.md` (in AyCode.Core repo)
|
||||
> For UI framework glossary see: `GLOSSARY.md` (in AyCode.Blazor repo)
|
||||
> For core measurement system rules and common domain traps, see: `../FruitBank.Common/docs/GLOSSARY.md`
|
||||
|
||||
Domain terminology for the FruitBank system. **Read this before making changes.**
|
||||
|
||||
## Business Domain
|
||||
|
||||
| English | Magyar | Definition |
|
||||
|---|---|---|
|
||||
| **Shipping** | Beszállítás | **INBOUND** delivery: supplier → warehouse. A truck arrival event. |
|
||||
| **Order** | Megrendelés | **OUTBOUND** delivery: warehouse → customer. |
|
||||
| **Pallet** (XxxItemPallet) | Mérési rekord | A **measurement record**, NOT a physical pallet. Always created, even for non-measurable products. |
|
||||
| **Partner** | Partner / Beszállító | External supplier providing goods. |
|
||||
| **ShippingDocument** | Szállítólevél | Supplier's delivery note or invoice, linked to a Shipping. |
|
||||
| **ShippingItem** | Szállítólevél tétel | Product line on a shipping document. Tracks declared vs measured discrepancies. |
|
||||
| **StockTaking** | Leltározás | Inventory session that freezes logical stock and reconciles with physical count. |
|
||||
| **GenericAttribute** | Generikus attribútum | nopCommerce polymorphic key-value store. KeyGroup = owner type, EntityId = owner ID. |
|
||||
|
||||
## nopCommerce Entities
|
||||
|
||||
These are **NOT custom FruitBank entities** — they come from nopCommerce:
|
||||
- Customer, Order, OrderItem, OrderNote, Product, GenericAttribute
|
||||
|
||||
FruitBank extends them via:
|
||||
- **DTOs** (OrderDto, OrderItemDto, ProductDto) that wrap nopCommerce entities with measurement properties
|
||||
- **GenericAttributes** for storing custom values (IsMeasurable, Tare, AverageWeight, etc.)
|
||||
|
||||
## UI / Grid Components
|
||||
|
||||
For MgGrid framework terms (MgGridBase, MgGridWithInfoPanel, MgGridToolbarBase, MgGridDataColumn, MgGridInfoPanel, IMgGridBase, etc.) see `GLOSSARY.md` (in AyCode.Blazor repo) and `AyCode.Blazor.Components/docs/MGGRID/README.md` (in AyCode.Blazor repo).
|
||||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **FruitBankGridBase** | Project-level adapter: fixes `TSignalRDataSource=SignalRDataSourceObservable`, `TId=int`, `TLoggerClient=LoggerClient`. Adds per-user layout, master/detail defaults. See `FruitBankHybrid.Shared/Components/Grids/README.md`. |
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# FruitBankHybridApp documentation
|
||||
|
||||
Top-level documentation for the `FruitBankHybridApp` repo (Layer 3 — FruitBank MAUI/Blazor Hybrid client).
|
||||
|
||||
## Reference docs (flat)
|
||||
|
||||
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — Repo architecture overview
|
||||
- [`CONVENTIONS.md`](CONVENTIONS.md) — Coding conventions
|
||||
- [`GLOSSARY.md`](GLOSSARY.md) — Domain glossary (FruitBank terms: Shipping, Order, StockTaking, MeasuringStatus, etc.)
|
||||
|
||||
## Sub-projects with docs
|
||||
|
||||
- `FruitBank.Common/docs/` — Common glossary (shared across Hybrid client-side)
|
||||
|
||||
## Navigation
|
||||
|
||||
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Single-file reference docs remain flat; multi-file topics would live in named subfolders (none currently at this level).
|
||||
|
||||
## See also
|
||||
|
||||
- **Server-side plugin**: `../../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/docs/README.md`
|
||||
- **Base framework** (AyCode.Core, AyCode.Blazor): see those repos' `docs/` folders.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env dotnet-script
|
||||
|
||||
#r "H:/Applications/Mango/Source/FruitBankHybridApp/FruitBank.Common/bin/Debug/net9.0/FruitBank.Common.dll"
|
||||
#r "H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core/bin/FruitBank/Debug/net9.0/AyCode.Core.dll"
|
||||
|
||||
using AyCode.Core.Serializers.Toons;
|
||||
using FruitBank.Common.Dtos;
|
||||
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||
Console.WriteLine(toon);
|
||||
|
||||
// Search for IsMeasurable property output
|
||||
if (toon.Contains("business-logic:"))
|
||||
{
|
||||
Console.WriteLine("\n✓ SUCCESS: business-logic attribute found!");
|
||||
var lines = toon.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Contains("IsMeasurable") || line.Contains("business-logic:"))
|
||||
{
|
||||
Console.WriteLine(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("\n✗ FAIL: business-logic attribute NOT found!");
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/h/Applications/Mango/Source/FruitBankHybridApp
|
||||
Loading…
Reference in New Issue