Add stock report features r

This commit is contained in:
2026-10-16 15:41:00 +05:30
parent c626f56cdb
commit d1d2d55352
16 changed files with 1291 additions and 136 deletions
@@ -0,0 +1,21 @@
namespace Distribution.Features.Vehicle;
public interface IVehicleDashboardRepository
{
Task<List<VehicleBalanceRow>> GetStockOnBoardAsync(Guid vehicleId);
Task<List<VehicleMovementRow>> GetMovementsAsync(Guid vehicleId, DateTime from, DateTime toExclusive);
Task<List<VehicleRideRow>> GetRidesAsync(Guid vehicleId, DateTime from, DateTime toExclusive);
Task<VehicleRideRow?> GetOpenRideAsync(Guid vehicleId);
Task<List<VehicleTaskRow>> GetTasksAsync(Guid vehicleId, DateTime from, DateTime toExclusive);
Task<List<VehicleCollectionRow>> GetCollectionsAsync(Guid vehicleId, DateTime from, DateTime toExclusive);
Task<VehicleReturnTotals> GetReturnTotalsAsync(Guid vehicleId, DateTime from, DateTime toExclusive);
Task<List<VehicleDriverRow>> GetDriversAsync(IEnumerable<Guid> driverProfileIds);
Task<VehicleNamedRef?> GetWarehouseRefAsync(Guid? warehouseId);
Task<VehicleNamedRef?> GetMobileWarehouseAsync(Guid vehicleId);
Task<Dictionary<Guid, string>> GetLocationNamesAsync(IEnumerable<Guid> ids);
Task<Dictionary<Guid, string>> GetUserNamesAsync(IEnumerable<Guid> ids);
}
@@ -4,7 +4,7 @@ namespace Distribution.Features.Vehicle;
[ApiController]
[Route("api/vehicles")]
public class VehicleController(VehicleService service) : ControllerBase
public class VehicleController(VehicleService service, VehicleDashboardService dashboardService) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetAll() =>
@@ -17,6 +17,15 @@ public class VehicleController(VehicleService service) : ControllerBase
return result is null ? NotFound() : Ok(result);
}
[HttpGet("{id:guid}/dashboard")]
public async Task<IActionResult> Dashboard(Guid id, [FromQuery] DateTime? from, [FromQuery] DateTime? to)
{
var error = VehicleDashboardService.Validate(from, to);
if (error is not null) return BadRequest(new { message = error });
var result = await dashboardService.GetAsync(id, from, to);
return result is null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> Create(CreateVehicleRequest request, CancellationToken ct)
{
@@ -0,0 +1,100 @@
using Distribution.Models;
namespace Distribution.Features.Vehicle;
// ── Repository projections ─────────────────────────────────────────────────
public record VehicleBalanceRow(
Guid ItemId, string Sku, string Name, string Category, string Uom,
decimal? SalePrice, decimal Quantity, DateTime UpdatedAt);
public record VehicleMovementRow(
Guid Id, Guid ItemId, string ItemName, string Sku,
Guid? FromLocationId, LocationType? FromLocationType,
Guid? ToLocationId, LocationType? ToLocationType,
decimal Quantity, MovementType Type, string? ReasonCode, Guid? ReferenceId,
DateTime CreatedAt, Guid CreatedBy);
public record VehicleRideRow(
Guid Id, RideStatus Status, Guid DriverProfileId, string DriverName,
int MeterStart, int MeterEnd, DateTime? StartedAt, DateTime? ClosedAt, DateTime CreatedAt,
CancelRequestType? CancelType, string? CancelReason,
int Tasks, int Delivered, int Failed);
public record VehicleTaskRow(
Guid RideId, DeliveryTaskStatus Status, Guid StoreId, string StoreName,
decimal DeliveredValue, DateTime Date);
public record VehicleCollectionRow(PaymentMethod Method, decimal Amount, int Count);
public record VehicleReturnTotals(int ReturnCount, decimal CreditValue, decimal Units);
public record VehicleDriverRow(
Guid Id, string Name, string LicenseNumber, DateTime LicenseExpiry, DateTime MedicalClearanceExpiry, bool IsActive);
public record VehicleNamedRef(Guid Id, string Name);
// ── Response ───────────────────────────────────────────────────────────────
public record VehicleDashboardPeriod(DateTime From, DateTime To);
public record VehicleDashboardKpis(
int Rides, int ClosedRides, int CancelledRides, decimal DistanceKm,
int Deliveries, int DeliveredCount, int FailedCount, int PendingCount, decimal SuccessRatePercent,
decimal DeliveredValue, decimal Collections,
decimal StockOnBoardUnits, decimal StockOnBoardValue,
decimal UnitsLoaded, decimal UnitsSold);
public record VehicleDashboardStockRow(
Guid ItemId, string Sku, string Name, string Category, string Uom,
decimal Quantity, decimal Value, DateTime UpdatedAt);
public record VehicleDashboardRide(
Guid Id, RideStatus Status, string DriverName, DateTime? StartedAt, DateTime? ClosedAt, DateTime CreatedAt,
decimal DistanceKm, int Tasks, int Delivered, int Failed, CancelRequestType? CancelType, string? CancelReason);
public record VehicleDashboardDailyDelivery(string Date, int Delivered, int Failed);
public record VehicleDashboardDailyMovement(string Date, decimal In, decimal Out);
public record VehicleDashboardMovementType(MovementType Type, string Direction, decimal Units, int Count);
public record VehicleDashboardLedgerRow(
Guid Id, DateTime CreatedAt, Guid ItemId, string ItemName, string Sku,
MovementType Type, string Direction, decimal Quantity,
string FromName, string ToName, Guid? ReferenceId, string? ReferenceKind,
string? ReasonCode, string CreatedByName);
public record VehicleDashboardCancelReason(CancelRequestType Type, int Count);
public record VehicleDashboardCollection(PaymentMethod Method, decimal Amount, int Count);
public record VehicleDashboardReturns(int ReturnCount, decimal CreditValue, decimal ReturnedUnits, decimal CustomerReturnUnitsOnVehicle);
public record VehicleDashboardTopStore(Guid StoreId, string StoreName, int Deliveries, decimal Value);
public record VehicleDashboardDriver(
Guid Id, string Name, string LicenseNumber, DateTime LicenseExpiry, DateTime MedicalClearanceExpiry,
bool IsActive, int Rides, decimal DistanceKm, DateTime? LastRideAt);
public record VehicleDashboardCurrentRide(
Guid Id, string DriverName, DateTime? StartedAt, int MeterStart, int Tasks, int Delivered, int Failed);
public record VehicleDashboardResponse(
VehicleResponse Vehicle,
VehicleNamedRef? DefaultWarehouse,
VehicleNamedRef? MobileWarehouse,
VehicleDashboardCurrentRide? CurrentRide,
VehicleDashboardPeriod Period,
VehicleDashboardKpis Kpis,
List<VehicleDashboardStockRow> StockOnBoard,
List<VehicleDashboardRide> Rides,
List<VehicleDashboardDailyDelivery> DailyDeliveries,
List<VehicleDashboardDailyMovement> DailyMovement,
List<VehicleDashboardMovementType> MovementByType,
List<VehicleDashboardLedgerRow> Ledger,
List<VehicleDashboardCancelReason> CancelReasons,
List<VehicleDashboardCollection> CollectionsByMethod,
VehicleDashboardReturns Returns,
List<VehicleDashboardTopStore> TopStores,
List<VehicleDashboardDriver> Drivers);
@@ -0,0 +1,130 @@
using Distribution.Data;
using Distribution.Models;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Vehicle;
public class VehicleDashboardRepository(AppDBContext db) : IVehicleDashboardRepository
{
public Task<List<VehicleBalanceRow>> GetStockOnBoardAsync(Guid vehicleId) =>
db.StockBalances.AsNoTracking()
.Where(b => b.LocationType == LocationType.Vehicle && b.LocationId == vehicleId && b.Quantity > 0)
.Select(b => new VehicleBalanceRow(
b.FK_ItemId, b.Item.Sku, b.Item.Name, b.Item.Category.Name, b.Item.BaseUom.Name,
b.Item.SalePrice, b.Quantity, b.UpdatedAt))
.ToListAsync();
public Task<List<VehicleMovementRow>> GetMovementsAsync(Guid vehicleId, DateTime from, DateTime toExclusive) =>
db.StockMovements.AsNoTracking()
.Where(m => (m.FromLocationId == vehicleId || m.ToLocationId == vehicleId)
&& m.CreatedAt >= from && m.CreatedAt < toExclusive)
.OrderByDescending(m => m.CreatedAt)
.Select(m => new VehicleMovementRow(
m.Id, m.FK_ItemId, m.Item.Name, m.Item.Sku,
m.FromLocationId, m.FromLocationType, m.ToLocationId, m.ToLocationType,
m.Quantity, m.Type, m.ReasonCode, m.ReferenceId, m.CreatedAt, m.CreatedBy))
.ToListAsync();
public Task<List<VehicleRideRow>> GetRidesAsync(Guid vehicleId, DateTime from, DateTime toExclusive) =>
db.DeliveryRuns.AsNoTracking()
.Where(r => r.FK_VehicleId == vehicleId
&& (r.StartedAt ?? r.CreatedAt) >= from && (r.StartedAt ?? r.CreatedAt) < toExclusive)
.OrderByDescending(r => r.StartedAt ?? r.CreatedAt)
.Select(r => new VehicleRideRow(
r.Id, r.Status, r.FK_DriverProfileId, r.DriverProfile.User.FullName,
r.Meterstart, r.Meterend, r.StartedAt, r.ClosedAt, r.CreatedAt,
r.CancelTypeCode, r.CancelReason,
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id),
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id
&& (t.Status == DeliveryTaskStatus.Delivered || t.Status == DeliveryTaskStatus.PartiallyDelivered)),
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id && t.Status == DeliveryTaskStatus.Failed)))
.ToListAsync();
public Task<VehicleRideRow?> GetOpenRideAsync(Guid vehicleId) =>
db.DeliveryRuns.AsNoTracking()
.Where(r => r.FK_VehicleId == vehicleId && r.Status == RideStatus.Open)
.Select(r => new VehicleRideRow(
r.Id, r.Status, r.FK_DriverProfileId, r.DriverProfile.User.FullName,
r.Meterstart, r.Meterend, r.StartedAt, r.ClosedAt, r.CreatedAt,
r.CancelTypeCode, r.CancelReason,
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id),
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id
&& (t.Status == DeliveryTaskStatus.Delivered || t.Status == DeliveryTaskStatus.PartiallyDelivered)),
db.DeliveryTasks.Count(t => t.FK_RideId == r.Id && t.Status == DeliveryTaskStatus.Failed)))
.FirstOrDefaultAsync();
public Task<List<VehicleTaskRow>> GetTasksAsync(Guid vehicleId, DateTime from, DateTime toExclusive) =>
db.DeliveryTasks.AsNoTracking()
.Where(t => t.Ride.FK_VehicleId == vehicleId
&& (t.Ride.StartedAt ?? t.Ride.CreatedAt) >= from && (t.Ride.StartedAt ?? t.Ride.CreatedAt) < toExclusive)
.Select(t => new VehicleTaskRow(
t.FK_RideId, t.Status, t.ShopPO.FK_StoreId, t.ShopPO.Store.StoreName,
t.ShopPO.Items.Sum(i => i.QuantityFulfilled * i.UnitPrice),
t.Ride.StartedAt ?? t.Ride.CreatedAt))
.ToListAsync();
public Task<List<VehicleCollectionRow>> GetCollectionsAsync(Guid vehicleId, DateTime from, DateTime toExclusive) =>
db.Payments.AsNoTracking()
.Where(p => p.CollectedAt >= from && p.CollectedAt < toExclusive
&& db.DeliveryTasks.Any(t => t.Ride.FK_VehicleId == vehicleId && t.FK_ShopPOId == p.Invoice.FK_ShopPOId))
.GroupBy(p => p.Method)
.Select(g => new VehicleCollectionRow(g.Key, g.Sum(p => p.Amount), g.Count()))
.ToListAsync();
public async Task<VehicleReturnTotals> GetReturnTotalsAsync(Guid vehicleId, DateTime from, DateTime toExclusive)
{
var returns = db.SaleReturns.AsNoTracking()
.Where(r => r.CreatedAt >= from && r.CreatedAt < toExclusive
&& db.DeliveryTasks.Any(t => t.Ride.FK_VehicleId == vehicleId && t.FK_ShopPOId == r.FK_ShopPOId));
var count = await returns.CountAsync();
var credit = await returns.SumAsync(r => (decimal?)r.TotalCreditAmount) ?? 0m;
var units = await returns.SelectMany(r => r.Items).SumAsync(i => (decimal?)i.Quantity) ?? 0m;
return new VehicleReturnTotals(count, credit, units);
}
public Task<List<VehicleDriverRow>> GetDriversAsync(IEnumerable<Guid> driverProfileIds)
{
var ids = driverProfileIds.Distinct().ToList();
return db.DriverProfiles.AsNoTracking()
.Where(d => ids.Contains(d.Id))
.Select(d => new VehicleDriverRow(
d.Id, d.User.FullName, d.LicenseNumber, d.LicenseExpiry, d.MedicalClearanceExpiry, d.IsActive))
.ToListAsync();
}
public Task<VehicleNamedRef?> GetWarehouseRefAsync(Guid? warehouseId) =>
warehouseId is null
? Task.FromResult<VehicleNamedRef?>(null)
: db.Warehouses.AsNoTracking()
.Where(w => w.Id == warehouseId)
.Select(w => new VehicleNamedRef(w.Id, w.Name))
.FirstOrDefaultAsync();
public Task<VehicleNamedRef?> GetMobileWarehouseAsync(Guid vehicleId) =>
db.Warehouses.AsNoTracking()
.Where(w => w.VehicleId == vehicleId)
.Select(w => new VehicleNamedRef(w.Id, w.Name))
.FirstOrDefaultAsync();
public async Task<Dictionary<Guid, string>> GetLocationNamesAsync(IEnumerable<Guid> ids)
{
var idList = ids.Distinct().ToList();
var names = new Dictionary<Guid, string>();
if (idList.Count == 0) return names;
foreach (var w in await db.Warehouses.AsNoTracking().Where(w => idList.Contains(w.Id)).Select(w => new { w.Id, w.Name }).ToListAsync())
names[w.Id] = w.Name;
foreach (var v in await db.Vehicles.AsNoTracking().Where(v => idList.Contains(v.Id)).Select(v => new { v.Id, v.RegNumber }).ToListAsync())
names[v.Id] = v.RegNumber;
return names;
}
public async Task<Dictionary<Guid, string>> GetUserNamesAsync(IEnumerable<Guid> ids)
{
var idList = ids.Distinct().ToList();
if (idList.Count == 0) return [];
return await db.Users.AsNoTracking()
.Where(u => idList.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.FullName);
}
}
@@ -0,0 +1,206 @@
using Distribution.Models;
namespace Distribution.Features.Vehicle;
public class VehicleDashboardService(IVehicleRepository vehicleRepo, IVehicleDashboardRepository repo)
{
private const int LedgerSize = 50;
private const int RideListSize = 15;
private const int TopListSize = 5;
private const int MaxPeriodDays = 366;
public static string? Validate(DateTime? from, DateTime? to)
{
if (from.HasValue && to.HasValue)
{
if (from.Value.Date > to.Value.Date) return "'from' must not be after 'to'.";
if ((to.Value.Date - from.Value.Date).TotalDays >= MaxPeriodDays) return $"The period cannot exceed {MaxPeriodDays} days.";
}
return null;
}
/// <summary>Unlike the standard vehicle lookup, this also serves inactive vehicles so their history stays viewable.</summary>
public async Task<VehicleDashboardResponse?> GetAsync(Guid id, DateTime? from, DateTime? to)
{
var vehicle = await vehicleRepo.GetByIdAsync(id);
if (vehicle is null) return null;
var toDate = (to ?? DateTime.Now).Date;
var fromDate = (from ?? toDate.AddDays(-29)).Date;
var toExclusive = toDate.AddDays(1);
var balances = await repo.GetStockOnBoardAsync(id);
var movements = (await repo.GetMovementsAsync(id, fromDate, toExclusive))
.Select(m => (Row: m, Direction: DirectionOf(m, id)))
.Where(x => x.Direction is not null)
.ToList();
var rides = await repo.GetRidesAsync(id, fromDate, toExclusive);
var tasks = await repo.GetTasksAsync(id, fromDate, toExclusive);
var collections = await repo.GetCollectionsAsync(id, fromDate, toExclusive);
var returnTotals = await repo.GetReturnTotalsAsync(id, fromDate, toExclusive);
var openRide = await repo.GetOpenRideAsync(id);
// ── Stock on board ─────────────────────────────────────────────────
var stock = balances
.GroupBy(b => b.ItemId)
.Select(g =>
{
var f = g.First();
var qty = g.Sum(x => x.Quantity);
return new VehicleDashboardStockRow(
f.ItemId, f.Sku, f.Name, f.Category, f.Uom, qty, qty * (f.SalePrice ?? 0m), g.Max(x => x.UpdatedAt));
})
.OrderBy(r => r.Name)
.ToList();
// ── Rides and deliveries ───────────────────────────────────────────
static decimal Km(VehicleRideRow r) =>
r.Status == RideStatus.Closed && r.MeterEnd > r.MeterStart ? r.MeterEnd - r.MeterStart : 0m;
var rideRows = rides.Take(RideListSize)
.Select(r => new VehicleDashboardRide(
r.Id, r.Status, r.DriverName, r.StartedAt, r.ClosedAt, r.CreatedAt,
Km(r), r.Tasks, r.Delivered, r.Failed, r.CancelType, r.CancelReason))
.ToList();
bool IsDelivered(VehicleTaskRow t) =>
t.Status is DeliveryTaskStatus.Delivered or DeliveryTaskStatus.PartiallyDelivered;
var delivered = tasks.Count(IsDelivered);
var failed = tasks.Count(t => t.Status == DeliveryTaskStatus.Failed);
var pending = tasks.Count(t => t.Status == DeliveryTaskStatus.Pending);
var deliveredValue = tasks.Where(IsDelivered).Sum(t => t.DeliveredValue);
var dailyDeliveries = new List<VehicleDashboardDailyDelivery>();
var dailyMovement = new List<VehicleDashboardDailyMovement>();
var tasksByDay = tasks.GroupBy(t => t.Date.Date).ToDictionary(g => g.Key);
var movesByDay = movements.GroupBy(x => x.Row.CreatedAt.Date).ToDictionary(g => g.Key);
for (var d = fromDate; d <= toDate; d = d.AddDays(1))
{
var key = d.ToString("yyyy-MM-dd");
tasksByDay.TryGetValue(d, out var dayTasks);
movesByDay.TryGetValue(d, out var dayMoves);
dailyDeliveries.Add(new VehicleDashboardDailyDelivery(
key, dayTasks?.Count(IsDelivered) ?? 0, dayTasks?.Count(t => t.Status == DeliveryTaskStatus.Failed) ?? 0));
dailyMovement.Add(new VehicleDashboardDailyMovement(
key,
dayMoves?.Where(x => x.Direction == "In").Sum(x => x.Row.Quantity) ?? 0m,
dayMoves?.Where(x => x.Direction == "Out").Sum(x => x.Row.Quantity) ?? 0m));
}
var cancelReasons = rides
.Where(r => r.Status == RideStatus.Cancelled && r.CancelType.HasValue)
.GroupBy(r => r.CancelType!.Value)
.Select(g => new VehicleDashboardCancelReason(g.Key, g.Count()))
.OrderByDescending(c => c.Count)
.ToList();
var topStores = tasks
.Where(IsDelivered)
.GroupBy(t => (t.StoreId, t.StoreName))
.Select(g => new VehicleDashboardTopStore(g.Key.StoreId, g.Key.StoreName, g.Count(), g.Sum(t => t.DeliveredValue)))
.OrderByDescending(s => s.Value)
.Take(TopListSize)
.ToList();
// ── Movements ──────────────────────────────────────────────────────
var byType = movements
.GroupBy(x => (x.Row.Type, x.Direction))
.Select(g => new VehicleDashboardMovementType(g.Key.Type, g.Key.Direction!, g.Sum(x => x.Row.Quantity), g.Count()))
.OrderByDescending(r => r.Units)
.ToList();
var ledgerSource = movements.Take(LedgerSize).ToList();
var locationIds = ledgerSource
.SelectMany(x => new[]
{
(x.Row.FromLocationId, x.Row.FromLocationType),
(x.Row.ToLocationId, x.Row.ToLocationType),
})
.Where(l => l.Item1.HasValue && l.Item2 is LocationType t && t != LocationType.External)
.Select(l => l.Item1!.Value);
var locationNames = await repo.GetLocationNamesAsync(locationIds);
var userNames = await repo.GetUserNamesAsync(ledgerSource.Select(x => x.Row.CreatedBy));
var ledger = ledgerSource.Select(x => new VehicleDashboardLedgerRow(
x.Row.Id, x.Row.CreatedAt, x.Row.ItemId, x.Row.ItemName, x.Row.Sku,
x.Row.Type, x.Direction!, x.Row.Quantity,
SideName(x.Row.Type, x.Row.FromLocationId, x.Row.FromLocationType, locationNames, isFrom: true),
SideName(x.Row.Type, x.Row.ToLocationId, x.Row.ToLocationType, locationNames, isFrom: false),
x.Row.ReferenceId, ReferenceKind(x.Row.Type),
x.Row.ReasonCode, userNames.GetValueOrDefault(x.Row.CreatedBy, "—"))).ToList();
// ── Drivers ────────────────────────────────────────────────────────
var driverProfiles = (await repo.GetDriversAsync(rides.Select(r => r.DriverProfileId)))
.ToDictionary(d => d.Id);
var drivers = rides
.GroupBy(r => r.DriverProfileId)
.Where(g => driverProfiles.ContainsKey(g.Key))
.Select(g =>
{
var d = driverProfiles[g.Key];
return new VehicleDashboardDriver(
d.Id, d.Name, d.LicenseNumber, d.LicenseExpiry, d.MedicalClearanceExpiry, d.IsActive,
g.Count(), g.Sum(Km), g.Max(r => r.StartedAt ?? r.CreatedAt));
})
.OrderByDescending(d => d.Rides)
.ToList();
// ── Assemble ───────────────────────────────────────────────────────
var settled = delivered + failed;
var kpis = new VehicleDashboardKpis(
rides.Count, rides.Count(r => r.Status == RideStatus.Closed), rides.Count(r => r.Status == RideStatus.Cancelled),
rides.Sum(Km),
tasks.Count, delivered, failed, pending,
settled == 0 ? 0m : Math.Round(delivered * 100m / settled, 1),
deliveredValue, collections.Sum(c => c.Amount),
stock.Sum(r => r.Quantity), stock.Sum(r => r.Value),
movements.Where(x => x.Row.Type == MovementType.Load && x.Direction == "In").Sum(x => x.Row.Quantity),
movements.Where(x => x.Row.Type == MovementType.Sale && x.Direction == "Out").Sum(x => x.Row.Quantity));
var customerReturnUnits = movements
.Where(x => x.Row.Type == MovementType.CustomerReturn && x.Direction == "In")
.Sum(x => x.Row.Quantity);
return new VehicleDashboardResponse(
new VehicleResponse(vehicle.Id, vehicle.RegNumber, vehicle.NumberPlate, vehicle.Type, vehicle.IsActive, vehicle.CreatedAt),
await repo.GetWarehouseRefAsync(vehicle.DefaultWarehouseId),
await repo.GetMobileWarehouseAsync(id),
openRide is null ? null : new VehicleDashboardCurrentRide(
openRide.Id, openRide.DriverName, openRide.StartedAt, openRide.MeterStart,
openRide.Tasks, openRide.Delivered, openRide.Failed),
new VehicleDashboardPeriod(fromDate, toDate),
kpis, stock, rideRows, dailyDeliveries, dailyMovement, byType, ledger, cancelReasons,
collections.Select(c => new VehicleDashboardCollection(c.Method, c.Amount, c.Count))
.OrderByDescending(c => c.Amount).ToList(),
new VehicleDashboardReturns(returnTotals.ReturnCount, returnTotals.CreditValue, returnTotals.Units, customerReturnUnits),
topStores, drivers);
}
private static string? DirectionOf(VehicleMovementRow m, Guid vehicleId) =>
m.ToLocationId == vehicleId && m.FromLocationId != vehicleId ? "In"
: m.FromLocationId == vehicleId && m.ToLocationId != vehicleId ? "Out"
: null;
private static string? ReferenceKind(MovementType type) => type switch
{
MovementType.Load => "Loading Sheet",
MovementType.Sale => "Order",
MovementType.CustomerReturn => "Return",
_ => null,
};
private static string SideName(
MovementType type, Guid? id, LocationType? locationType, Dictionary<Guid, string> names, bool isFrom)
{
if (id is not Guid locId || locationType is not LocationType lt) return "—";
if (lt != LocationType.External) return names.GetValueOrDefault(locId, "—");
return type switch
{
MovementType.Sale or MovementType.CustomerReturn => "Customer",
MovementType.PurchaseReceipt or MovementType.PurchaseReturn => "Supplier",
MovementType.Damage => "Written Off",
MovementType.Adjustment => "Stock Adjustment",
_ => isFrom ? "External Source" : "External Destination",
};
}
}