diff --git a/FruitBank.Common/Entities/CargoPartner.cs b/FruitBank.Common/Entities/CargoPartner.cs index cd3042a7..da676386 100644 --- a/FruitBank.Common/Entities/CargoPartner.cs +++ b/FruitBank.Common/Entities/CargoPartner.cs @@ -7,7 +7,7 @@ using Mango.Nop.Core.Entities; namespace FruitBank.Common.Entities; [AcBinarySerializable(false, true, false, true, false, false)] -//[ToonDescription("Business partner with address and tax information", Purpose = "Represents an external legal entity, specifically a Supplier who provides goods or a business partner involved in the procurement chain")] +[ToonDescription("Transport / haulage company (carrier) with its vehicle fleet", Purpose = "A carrier that delivers goods to the warehouse and owns the CargoTrucks (both trucks and trailers). Distinct from Partner, which is the goods supplier. Name, address and tax fields are inherited from PartnerBase.")] [Table(Name = FruitBankConstClient.CargoPartnerDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.CargoPartnerDbTableName)] public sealed class CargoPartner : PartnerBase, ICargoPartner @@ -16,6 +16,5 @@ public sealed class CargoPartner : PartnerBase, ICargoPartner public List? CargoTrucks { get; set; } //[Association(ThisKey = nameof(Id), OtherKey = nameof(Shipping.CargoPartnerId), CanBeNull = true)] - public List? Shippings { get; set; } - //public decimal DecProp{get; set; } + //public List? Shippings { get; set; } } \ No newline at end of file diff --git a/FruitBank.Common/Entities/CargoTruck.cs b/FruitBank.Common/Entities/CargoTruck.cs index f9f791db..90c6d5c5 100644 --- a/FruitBank.Common/Entities/CargoTruck.cs +++ b/FruitBank.Common/Entities/CargoTruck.cs @@ -1,16 +1,20 @@ -using AyCode.Core.Serializers.Attributes; +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; 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.CargoTruckDbTableName)] +[ToonDescription("Cargo vehicle — truck or trailer — owned by a transport partner", Purpose = "A single vehicle in a transport company's fleet. One table holds both tractor units and trailers, distinguished by IsTrailer. Truck and trailer are tracked as separate vehicles because Hungarian EKÁER road-freight reporting (NAV) declares the towing vehicle and the trailer as distinct entries (vehicle / vehicle2), each with its own licence plate and country code — i.e. these shipments carry an EKÁER declaration obligation.")] +[LinqToDB.Mapping.Table(Name = FruitBankConstClient.CargoTruckDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.CargoTruckDbTableName)] public sealed class CargoTruck: MgEntityBase, ICargoTruck { + [ToonDescription(Purpose = "FK to the owning transport company (CargoPartner) — the carrier, not the goods supplier.")] public int CargoPartnerId { get; set; } [Association(ThisKey = nameof(CargoPartnerId), OtherKey = nameof(CargoPartner.Id), CanBeNull = true)] @@ -18,8 +22,13 @@ public sealed class CargoTruck: MgEntityBase, ICargoTruck public string CountryCode { get; set; } public string LicencePlate { get; set; } + + [ToonDescription(Purpose = "Discriminates the shared table: false = tractor/truck unit, true = trailer.")] public bool IsTrailer { get; set; } + [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] + public string? CargoPartnerName => CargoPartner?.Name; + public DateTime Created { get; set; } public DateTime Modified { get; set; } } diff --git a/FruitBank.Common/Entities/README.md b/FruitBank.Common/Entities/README.md index e3d2d811..0dc24a86 100644 --- a/FruitBank.Common/Entities/README.md +++ b/FruitBank.Common/Entities/README.md @@ -11,6 +11,11 @@ Domain entities for inbound/outbound goods tracking and inventory. All map to `f - **`ShippingDocumentToFiles.cs`** — Many-to-many link: document ↔ file with DocumentType. Table: `fbShippingDocumentToFiles`. - **`Partner.cs`** — External supplier with address and tax info. Table: `fbPartner`. +## Cargo / Logistics + +- **`CargoPartner.cs`** — Freight/haulage partner (carrier). Distinct from `Partner` (supplier) — this is the transport side. Has `CargoTrucks` and `Shippings` collections. Table: `fbCargoPartner`. +- **`CargoTruck.cs`** — Individual truck belonging to a `CargoPartner` (`LicencePlate`, `CountryCode`, `IsTrailer` for trailers). Table: `fbCargoTruck`. + ## Order (Outbound) - **`OrderItemPallet.cs`** — Measurement record for outgoing goods with RevisorId for audit. Table: `fbOrderItemPallet`. diff --git a/FruitBank.Common/Entities/Shipping.cs b/FruitBank.Common/Entities/Shipping.cs index 85e4ae45..b41cbbb7 100644 --- a/FruitBank.Common/Entities/Shipping.cs +++ b/FruitBank.Common/Entities/Shipping.cs @@ -13,6 +13,10 @@ namespace FruitBank.Common.Entities; [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)] public sealed class Shipping : MgEntityBase, IShipping, IEntityComment { + public int CargoPartnerId { get; set; } + public int? CargoTruckId { get; set; } + public int? CargoTrailerId { get; set; } + public DateTime ShippingDate { get; set; } = DateTime.Now; public string LicencePlate { get; set; } public bool IsAllMeasured { get; set; } @@ -21,6 +25,18 @@ public sealed class Shipping : MgEntityBase, IShipping, IEntityComment public DateTime? MeasuredDate { get; set; } + [ToonDescription(Purpose = "The transport company (carrier) that hauled this delivery — distinct from the goods supplier, which is reached via ShippingDocument.Partner.")] + [Association(ThisKey = nameof(CargoPartnerId), OtherKey = nameof(CargoPartner.Id), CanBeNull = true)] + public CargoPartner CargoPartner { get; set; } + + [ToonDescription(Purpose = "The tractor/truck unit — a CargoTruck row with IsTrailer=false.")] + [Association(ThisKey = nameof(CargoTruckId), OtherKey = nameof(CargoTruck.Id), CanBeNull = true)] + public CargoTruck CargoTruck { get; set; } + + [ToonDescription(Purpose = "The trailer — the SAME CargoTruck table as CargoTruck, but the row with IsTrailer=true (CargoTrailerId → CargoTruck.Id).")] + [Association(ThisKey = nameof(CargoTrailerId), OtherKey = nameof(CargoTruck.Id), CanBeNull = true)] + public CargoTruck CargoTrailer { get; set; } + [Association(ThisKey = nameof(Id), OtherKey = nameof(ShippingDocument.ShippingId), CanBeNull = true)] public List? ShippingDocuments { get; set; } diff --git a/FruitBank.Common/Interfaces/ICargoTruck.cs b/FruitBank.Common/Interfaces/ICargoTruck.cs index 538d8612..d3e9001b 100644 --- a/FruitBank.Common/Interfaces/ICargoTruck.cs +++ b/FruitBank.Common/Interfaces/ICargoTruck.cs @@ -14,4 +14,6 @@ public interface ICargoTruck : IEntityInt, ITimeStampInfo public string LicencePlate { get; set; } public bool IsTrailer { get; set; } + + public string? CargoPartnerName { get; } } \ No newline at end of file diff --git a/FruitBank.Common/Interfaces/IShipping.cs b/FruitBank.Common/Interfaces/IShipping.cs index 25b1c563..73bf5ab7 100644 --- a/FruitBank.Common/Interfaces/IShipping.cs +++ b/FruitBank.Common/Interfaces/IShipping.cs @@ -6,6 +6,10 @@ namespace FruitBank.Common.Interfaces; public interface IShipping : IEntityInt, ITimeStampInfo//, IMeasured { + public int CargoPartnerId { get; set; } + public int? CargoTruckId { get; set; } + public int? CargoTrailerId { get; set; } + DateTime ShippingDate { get; set; } string LicencePlate { get; set; } bool IsAllMeasured { get; set; } diff --git a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor index 1f0730c8..b408b12c 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Cargos/GridCargoPartner.razor @@ -45,7 +45,7 @@ @if (IsMasterGrid) { var cargoPartner = ((CargoPartner)context.DataItem); - var shippings = cargoPartner?.Shippings ?? []; + var shippings = new AcObservableCollection(); //cargoPartner?.Shippings ?? []; var cargoTrucks = cargoPartner?.CargoTrucks ?? []; @@ -56,13 +56,13 @@ } - +@* @{ - var observableShippings = new AcObservableCollection(shippings); - + //var observableShippings = new AcObservableCollection(shippings); + } - + *@ } @@ -83,6 +83,7 @@ [Parameter] public AcObservableCollection? CargoPartners { get; set; } [Parameter] public AcObservableCollection? Shippings { get; set; } + const string ExportFileName = "ExportResult"; string GridSearchText = ""; bool EditItemsEnabled { get; set; } diff --git a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor index 03564592..c188f704 100644 --- a/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor +++ b/FruitBankHybrid.Shared/Components/Grids/Shippings/GridShipping.razor @@ -7,6 +7,7 @@ @using FruitBank.Common.Entities @using FruitBankHybrid.Shared.Components.Grids.Shippings @using FruitBankHybrid.Shared.Databases +@using FruitBankHybrid.Shared.Extensions @using FruitBankHybrid.Shared.Services.Loggers @using FruitBankHybrid.Shared.Services.SignalRs @@ -19,11 +20,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -71,7 +129,11 @@ [Parameter] public bool IsMasterGrid { get; set; } = false; [Parameter] public AcObservableCollection? Partners { get; set; } + [Parameter] public AcObservableCollection? CargoPartners { get; set; } [Parameter] public AcObservableCollection? Shippings { get; set; } + [Inject] public required IDialogService DialogService { get; set; } = null!; + + public List CargoTrucks { get; set; } = []; const string ExportFileName = "ExportResult"; string GridSearchText = ""; @@ -91,6 +153,23 @@ private async Task ReloadDataFromDb(bool forceReload = false) { + using (await ObjectLock.GetSemaphore().UseWaitAsync()) + { + if (CargoPartners == null) CargoPartners = new AcObservableCollection(await FruitBankSignalRClient.GetCargoPartners() ?? []); + else if (CargoPartners.Count == 0 || forceReload) + { + CargoPartners.Replace(await FruitBankSignalRClient.GetCargoPartners() ?? []); + } + } + + CargoTrucks = CargoPartners.Where(x => x.CargoTrucks != null).SelectMany(x => + { + if (x.CargoTrucks == null) return null; + + foreach (var cargoTruck in x.CargoTrucks) cargoTruck.CargoPartner = x; + return x.CargoTrucks; + }).ToList(); + if (!IsMasterGrid) return; using (await ObjectLock.GetSemaphore().UseWaitAsync()) @@ -121,7 +200,23 @@ EditItemsEnabled = true; } - protected async Task OnActiveTabChanged(int activeTabIndex) + async Task Grid_EditModelSaving(GridEditModelSavingEventArgs e) + { + var shipping = (Shipping)e.EditModel; // a szerkesztett sor + + if (ValidateCargoConsistency(shipping, out string error)) return; + + e.Cancel = true; + await DialogService.ShowMessageBoxAsync("Hibás adat", error, MessageBoxRenderStyle.Danger); + // e.IsNew → add vs update + + // ... before-save logika (validáció, számított mezők, CargoTrailer beállítás stb.) + + // megszakítás, ha kell: + // e.Cancel = true; + } + + async protected Task OnActiveTabChanged(int activeTabIndex) { _activeTabIndex = activeTabIndex; return; @@ -142,5 +237,29 @@ // break; // } } + + private bool ValidateCargoConsistency(Shipping shipping, out string? error) + { + error = null; + + if (shipping.CargoTruckId != null) + { + var truck = CargoTrucks.FirstOrDefault(t => t.Id == shipping.CargoTruckId); + if (truck == null || truck.CargoPartnerId != shipping.CargoPartnerId) + { + error = $"A vontató ({truck?.LicencePlate}) nem a kiválasztott fuvarozóhoz tartozik!"; + return false; + } + } + + if (shipping.CargoTrailerId == null) return true; + + var trailer = CargoTrucks.FirstOrDefault(t => t.Id == shipping.CargoTrailerId); + if (trailer != null && trailer.CargoPartnerId == shipping.CargoPartnerId) return true; + + error = $"Az utánfutó ({trailer?.LicencePlate}) nem a kiválasztott fuvarozóhoz tartozik!"; + return false; + } + } diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index dd1482c5..42597514 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -14,6 +14,8 @@ Domain terminology for the FruitBank system. **Read this before making changes.* | **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. | +| **CargoPartner** | Fuvarozó | Freight carrier company that transports goods. **Distinct from Partner** (supplier). | +| **CargoTruck** | Kamion | A specific truck belonging to a CargoPartner (licence plate, trailer flag). | | **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. |