Merge pull request 'Refactor/ride lifecycle' (#26) from refactor/ride_lifecycle into dev
Reviewed-on: #26
This commit was merged in pull request #26.
This commit is contained in:
@@ -37,7 +37,6 @@ public class AppDBContext(DbContextOptions<AppDBContext> options) : DbContext(op
|
|||||||
public DbSet<Supplier> Suppliers { get; set; }
|
public DbSet<Supplier> Suppliers { get; set; }
|
||||||
public DbSet<DriverProfile> DriverProfiles { get; set; }
|
public DbSet<DriverProfile> DriverProfiles { get; set; }
|
||||||
public DbSet<DriverComplianceConfig> DriverComplianceConfigs { get; set; }
|
public DbSet<DriverComplianceConfig> DriverComplianceConfigs { get; set; }
|
||||||
public DbSet<RideCancelRequest> RideCancelRequests { get; set; }
|
|
||||||
public DbSet<Scheme> Schemes { get; set; }
|
public DbSet<Scheme> Schemes { get; set; }
|
||||||
public DbSet<SchemeRedemption> SchemeRedemptions { get; set; }
|
public DbSet<SchemeRedemption> SchemeRedemptions { get; set; }
|
||||||
public DbSet<SchemeBundleComponent> SchemeBundleComponents { get; set; }
|
public DbSet<SchemeBundleComponent> SchemeBundleComponents { get; set; }
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public static class CommerceSeed
|
|||||||
private static readonly Guid SchemeRedemption1Id = new("50000000-0000-0000-0000-000000000001");
|
private static readonly Guid SchemeRedemption1Id = new("50000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
private static readonly Guid Payment1Id = new("47000000-0000-0000-0000-000000000001");
|
private static readonly Guid Payment1Id = new("47000000-0000-0000-0000-000000000001");
|
||||||
|
private static readonly Guid Payment2Id = new("47000000-0000-0000-0000-000000000002");
|
||||||
|
private static readonly Guid Payment3Id = new("47000000-0000-0000-0000-000000000003");
|
||||||
|
private static readonly Guid Payment4Id = new("47000000-0000-0000-0000-000000000004");
|
||||||
|
|
||||||
private static readonly Guid SaleReturn1Id = new("51000000-0000-0000-0000-000000000001");
|
private static readonly Guid SaleReturn1Id = new("51000000-0000-0000-0000-000000000001");
|
||||||
private static readonly Guid SaleReturnItem1Id = new("52000000-0000-0000-0000-000000000001");
|
private static readonly Guid SaleReturnItem1Id = new("52000000-0000-0000-0000-000000000001");
|
||||||
@@ -84,7 +87,10 @@ public static class CommerceSeed
|
|||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<Payment>().HasData(
|
modelBuilder.Entity<Payment>().HasData(
|
||||||
new Payment { Id = Payment1Id, FK_InvoiceId = OperationsDataSeed.Invoice1Id, Amount = 3000m, Method = PaymentMethod.Cash, CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0), CollectedBy = MasterDataSeed.SalesRep1UserId, CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0), IsActive = true }
|
new Payment { Id = Payment1Id, FK_InvoiceId = OperationsDataSeed.Invoice1Id, Amount = 3000m, Method = PaymentMethod.Cash, EntryMode = EntryMode.RealTime, CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0), CollectedBy = MasterDataSeed.SalesRep1UserId, CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0), IsActive = true },
|
||||||
|
new Payment { Id = Payment2Id, FK_InvoiceId = OperationsDataSeed.Invoice2Id, Amount = 1000m, Method = PaymentMethod.Cash, EntryMode = EntryMode.RealTime, CollectedAt = new DateTime(2026, 8, 31, 9, 20, 0), CollectedBy = MasterDataSeed.SalesRep2UserId, CreatedAt = new DateTime(2026, 8, 31, 9, 20, 0), IsActive = true },
|
||||||
|
new Payment { Id = Payment3Id, FK_InvoiceId = OperationsDataSeed.Invoice2Id, Amount = 800m, Method = PaymentMethod.Cheque, EntryMode = EntryMode.DayEndManual, ChequeNumber = "000456", ChequeDate = new DateTime(2026, 8, 31), CollectedAt = new DateTime(2026, 8, 31, 11, 0, 0), CollectedBy = MasterDataSeed.SalesRep2UserId, RecordedBy = MasterDataSeed.Supervisor1UserId, CreatedAt = new DateTime(2026, 8, 31, 18, 0, 0), IsActive = true },
|
||||||
|
new Payment { Id = Payment4Id, FK_InvoiceId = OperationsDataSeed.Invoice2Id, Amount = 300m, Method = PaymentMethod.Credit, EntryMode = EntryMode.RealTime, CollectedAt = new DateTime(2026, 8, 31, 11, 5, 0), CollectedBy = MasterDataSeed.SalesRep2UserId, CreatedAt = new DateTime(2026, 8, 31, 11, 5, 0), IsActive = true }
|
||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<SaleReturn>().HasData(
|
modelBuilder.Entity<SaleReturn>().HasData(
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ public static class OperationsDataSeed
|
|||||||
|
|
||||||
internal static readonly Guid ShopPO1Id = new("33000000-0000-0000-0000-000000000001");
|
internal static readonly Guid ShopPO1Id = new("33000000-0000-0000-0000-000000000001");
|
||||||
internal static readonly Guid ShopPO2Id = new("33000000-0000-0000-0000-000000000002");
|
internal static readonly Guid ShopPO2Id = new("33000000-0000-0000-0000-000000000002");
|
||||||
|
internal static readonly Guid ShopPO3Id = new("33000000-0000-0000-0000-000000000003");
|
||||||
|
|
||||||
internal static readonly Guid ShopPOItem1Id = new("34000000-0000-0000-0000-000000000001");
|
internal static readonly Guid ShopPOItem1Id = new("34000000-0000-0000-0000-000000000001");
|
||||||
internal static readonly Guid ShopPOItem2Id = new("34000000-0000-0000-0000-000000000002");
|
internal static readonly Guid ShopPOItem2Id = new("34000000-0000-0000-0000-000000000002");
|
||||||
internal static readonly Guid ShopPOItem3Id = new("34000000-0000-0000-0000-000000000003");
|
internal static readonly Guid ShopPOItem3Id = new("34000000-0000-0000-0000-000000000003");
|
||||||
|
internal static readonly Guid ShopPOItem4Id = new("34000000-0000-0000-0000-000000000004");
|
||||||
|
|
||||||
internal static readonly Guid Invoice1Id = new("34500000-0000-0000-0000-000000000001");
|
internal static readonly Guid Invoice1Id = new("34500000-0000-0000-0000-000000000001");
|
||||||
|
internal static readonly Guid Invoice2Id = new("34500000-0000-0000-0000-000000000002");
|
||||||
|
|
||||||
internal static readonly Guid LoadingSheet1Id = new("35000000-0000-0000-0000-000000000001");
|
internal static readonly Guid LoadingSheet1Id = new("35000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
@@ -86,13 +89,28 @@ public static class OperationsDataSeed
|
|||||||
FulfilledAt = null,
|
FulfilledAt = null,
|
||||||
CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0),
|
CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0),
|
||||||
IsActive = true
|
IsActive = true
|
||||||
|
},
|
||||||
|
new ShopPO
|
||||||
|
{
|
||||||
|
Id = ShopPO3Id,
|
||||||
|
FK_StoreId = MasterDataSeed.KandyStoreId,
|
||||||
|
RaisedById = MasterDataSeed.SalesRep2UserId,
|
||||||
|
InvoiceId = Invoice2Id,
|
||||||
|
TotalAmount = 2400m,
|
||||||
|
Status = ShopPOStatus.Fulfilled,
|
||||||
|
ConfirmedAt = new DateTime(2026, 8, 31, 8, 30, 0),
|
||||||
|
ConfirmedBy = MasterDataSeed.Supervisor1UserId,
|
||||||
|
FulfilledAt = new DateTime(2026, 8, 31, 13, 0, 0),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0),
|
||||||
|
IsActive = true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<ShopPOItem>().HasData(
|
modelBuilder.Entity<ShopPOItem>().HasData(
|
||||||
new ShopPOItem { Id = ShopPOItem1Id, FK_ShopPOId = ShopPO1Id, FK_ItemId = MasterDataSeed.Coke500ItemId, Quantity = 30, QuantityFulfilled = 30, UnitPrice = 60m, CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
new ShopPOItem { Id = ShopPOItem1Id, FK_ShopPOId = ShopPO1Id, FK_ItemId = MasterDataSeed.Coke500ItemId, Quantity = 30, QuantityFulfilled = 30, UnitPrice = 60m, CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
||||||
new ShopPOItem { Id = ShopPOItem2Id, FK_ShopPOId = ShopPO1Id, FK_ItemId = MasterDataSeed.LaysClassicItemId, Quantity = 24, QuantityFulfilled = 24, UnitPrice = 50m, CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
new ShopPOItem { Id = ShopPOItem2Id, FK_ShopPOId = ShopPO1Id, FK_ItemId = MasterDataSeed.LaysClassicItemId, Quantity = 24, QuantityFulfilled = 24, UnitPrice = 50m, CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
||||||
new ShopPOItem { Id = ShopPOItem3Id, FK_ShopPOId = ShopPO2Id, FK_ItemId = MasterDataSeed.Coke1500ItemId, Quantity = 10, QuantityFulfilled = 0, UnitPrice = 120m, CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0), IsActive = true }
|
new ShopPOItem { Id = ShopPOItem3Id, FK_ShopPOId = ShopPO2Id, FK_ItemId = MasterDataSeed.Coke1500ItemId, Quantity = 10, QuantityFulfilled = 0, UnitPrice = 120m, CreatedAt = new DateTime(2026, 8, 30, 10, 0, 0), IsActive = true },
|
||||||
|
new ShopPOItem { Id = ShopPOItem4Id, FK_ShopPOId = ShopPO3Id, FK_ItemId = MasterDataSeed.Coke1500ItemId, Quantity = 20, QuantityFulfilled = 20, UnitPrice = 120m, CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0), IsActive = true }
|
||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<Invoice>().HasData(
|
modelBuilder.Entity<Invoice>().HasData(
|
||||||
@@ -110,6 +128,21 @@ public static class OperationsDataSeed
|
|||||||
TotalAmount = 3000m,
|
TotalAmount = 3000m,
|
||||||
CreatedAt = new DateTime(2026, 8, 25, 8, 30, 0),
|
CreatedAt = new DateTime(2026, 8, 25, 8, 30, 0),
|
||||||
IsActive = true
|
IsActive = true
|
||||||
|
},
|
||||||
|
new Invoice
|
||||||
|
{
|
||||||
|
Id = Invoice2Id,
|
||||||
|
FK_ShopPOId = ShopPO3Id,
|
||||||
|
Status = InvoiceStatus.Printed,
|
||||||
|
PaymentStatus = PaymentStatus.PartiallyPaid,
|
||||||
|
CreatedBy = MasterDataSeed.SalesRep2UserId,
|
||||||
|
PrintedAt = new DateTime(2026, 8, 31, 9, 15, 0),
|
||||||
|
PrintedBy = MasterDataSeed.SalesRep2UserId,
|
||||||
|
FinalizedAt = null,
|
||||||
|
FinalizedBy = null,
|
||||||
|
TotalAmount = 2400m,
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0),
|
||||||
|
IsActive = true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ public static class OperationsExtrasSeed
|
|||||||
|
|
||||||
private static readonly Guid DriverComplianceConfig1Id = new("64000000-0000-0000-0000-000000000001");
|
private static readonly Guid DriverComplianceConfig1Id = new("64000000-0000-0000-0000-000000000001");
|
||||||
|
|
||||||
private static readonly Guid RideCancelRequest1Id = new("65000000-0000-0000-0000-000000000001");
|
|
||||||
|
|
||||||
private static readonly Guid RouteVisitLog1Id = new("66000000-0000-0000-0000-000000000001");
|
private static readonly Guid RouteVisitLog1Id = new("66000000-0000-0000-0000-000000000001");
|
||||||
private static readonly Guid RouteVisitLog2Id = new("66000000-0000-0000-0000-000000000002");
|
private static readonly Guid RouteVisitLog2Id = new("66000000-0000-0000-0000-000000000002");
|
||||||
|
|
||||||
@@ -95,21 +93,6 @@ public static class OperationsExtrasSeed
|
|||||||
new DriverComplianceConfig { Id = DriverComplianceConfig1Id, EnforceLicenseExpiry = true, EnforceMedicalClearance = true, UpdatedAt = new DateTime(2026, 8, 20), UpdatedBy = MasterDataSeed.Supervisor1UserId }
|
new DriverComplianceConfig { Id = DriverComplianceConfig1Id, EnforceLicenseExpiry = true, EnforceMedicalClearance = true, UpdatedAt = new DateTime(2026, 8, 20), UpdatedBy = MasterDataSeed.Supervisor1UserId }
|
||||||
);
|
);
|
||||||
|
|
||||||
modelBuilder.Entity<RideCancelRequest>().HasData(
|
|
||||||
new RideCancelRequest
|
|
||||||
{
|
|
||||||
Id = RideCancelRequest1Id,
|
|
||||||
FK_RideId = OperationsDataSeed.ActiveDeliveryRunId,
|
|
||||||
TypeCode = CancelRequestType.VehicleBreakdown,
|
|
||||||
Reason = "Reported minor engine issue, resolved after inspection",
|
|
||||||
RequestedBy = MasterDataSeed.Driver2UserId,
|
|
||||||
RequestedAt = new DateTime(2026, 8, 30, 9, 10, 0),
|
|
||||||
Status = CancelRequestStatus.Rejected,
|
|
||||||
ApprovedBy = MasterDataSeed.Supervisor1UserId,
|
|
||||||
ApprovedAt = new DateTime(2026, 8, 30, 9, 15, 0)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
modelBuilder.Entity<RouteVisitLog>().HasData(
|
modelBuilder.Entity<RouteVisitLog>().HasData(
|
||||||
new RouteVisitLog { Id = RouteVisitLog1Id, FK_RouteId = MasterDataSeed.ColomboRouteId, FK_StoreId = MasterDataSeed.FortStoreId, FK_UserId = MasterDataSeed.SalesRep1UserId, CheckInAt = new DateTime(2026, 8, 25, 9, 0, 0), CheckInLatitude = 6.9344m, CheckInLongitude = 79.8428m, IsDeviationFlagged = false, CheckOutAt = new DateTime(2026, 8, 25, 9, 25, 0), CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
new RouteVisitLog { Id = RouteVisitLog1Id, FK_RouteId = MasterDataSeed.ColomboRouteId, FK_StoreId = MasterDataSeed.FortStoreId, FK_UserId = MasterDataSeed.SalesRep1UserId, CheckInAt = new DateTime(2026, 8, 25, 9, 0, 0), CheckInLatitude = 6.9344m, CheckInLongitude = 79.8428m, IsDeviationFlagged = false, CheckOutAt = new DateTime(2026, 8, 25, 9, 25, 0), CreatedAt = new DateTime(2026, 8, 25, 9, 0, 0), IsActive = true },
|
||||||
new RouteVisitLog { Id = RouteVisitLog2Id, FK_RouteId = MasterDataSeed.ColomboRouteId, FK_StoreId = MasterDataSeed.DehiwalaStoreId, FK_UserId = MasterDataSeed.SalesRep1UserId, CheckInAt = new DateTime(2026, 8, 25, 10, 0, 0), CheckInLatitude = 6.9000m, CheckInLongitude = 79.8654m, IsDeviationFlagged = true, CheckOutAt = null, CreatedAt = new DateTime(2026, 8, 25, 10, 0, 0), IsActive = true }
|
new RouteVisitLog { Id = RouteVisitLog2Id, FK_RouteId = MasterDataSeed.ColomboRouteId, FK_StoreId = MasterDataSeed.DehiwalaStoreId, FK_UserId = MasterDataSeed.SalesRep1UserId, CheckInAt = new DateTime(2026, 8, 25, 10, 0, 0), CheckInLatitude = 6.9000m, CheckInLongitude = 79.8654m, IsDeviationFlagged = true, CheckOutAt = null, CreatedAt = new DateTime(2026, 8, 25, 10, 0, 0), IsActive = true }
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ namespace Distribution.Features.Payment;
|
|||||||
public record CreatePaymentRequest(
|
public record CreatePaymentRequest(
|
||||||
decimal Amount,
|
decimal Amount,
|
||||||
PaymentMethod Method,
|
PaymentMethod Method,
|
||||||
|
EntryMode EntryMode = EntryMode.RealTime,
|
||||||
|
string? ChequeNumber = null,
|
||||||
|
DateTime? ChequeDate = null,
|
||||||
Guid? CollectedByUserId = null);
|
Guid? CollectedByUserId = null);
|
||||||
|
|
||||||
public record PaymentResponse(
|
public record PaymentResponse(
|
||||||
@@ -12,6 +15,10 @@ public record PaymentResponse(
|
|||||||
Guid InvoiceId,
|
Guid InvoiceId,
|
||||||
decimal Amount,
|
decimal Amount,
|
||||||
PaymentMethod Method,
|
PaymentMethod Method,
|
||||||
|
EntryMode EntryMode,
|
||||||
|
string? ChequeNumber,
|
||||||
|
DateTime? ChequeDate,
|
||||||
DateTime CollectedAt,
|
DateTime CollectedAt,
|
||||||
Guid CollectedBy,
|
Guid CollectedBy,
|
||||||
|
Guid? RecordedBy,
|
||||||
DateTime CreatedAt);
|
DateTime CreatedAt);
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public class PaymentService(
|
|||||||
ICurrentUserService currentUser)
|
ICurrentUserService currentUser)
|
||||||
{
|
{
|
||||||
private static PaymentResponse ToResponse(Models.Payment p) =>
|
private static PaymentResponse ToResponse(Models.Payment p) =>
|
||||||
new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.CollectedAt, p.CollectedBy, p.CreatedAt);
|
new(p.Id, p.FK_InvoiceId, p.Amount, p.Method, p.EntryMode, p.ChequeNumber, p.ChequeDate,
|
||||||
|
p.CollectedAt, p.CollectedBy, p.RecordedBy, p.CreatedAt);
|
||||||
|
|
||||||
public async Task<List<PaymentResponse>?> GetByInvoiceIdAsync(Guid invoiceId)
|
public async Task<List<PaymentResponse>?> GetByInvoiceIdAsync(Guid invoiceId)
|
||||||
{
|
{
|
||||||
@@ -30,16 +31,28 @@ public class PaymentService(
|
|||||||
if (invoice.Status == InvoiceStatus.Finalized)
|
if (invoice.Status == InvoiceStatus.Finalized)
|
||||||
throw new InvalidOperationException("Cannot record a payment against a finalized invoice");
|
throw new InvalidOperationException("Cannot record a payment against a finalized invoice");
|
||||||
|
|
||||||
var userId = currentUser.UserId ?? request.CollectedByUserId
|
if (request.Method == PaymentMethod.Cheque &&
|
||||||
|
(string.IsNullOrWhiteSpace(request.ChequeNumber) || request.ChequeDate is null))
|
||||||
|
throw new InvalidOperationException("ChequeNumber and ChequeDate are required when Method is Cheque");
|
||||||
|
|
||||||
|
var recorderId = currentUser.UserId ?? request.CollectedByUserId
|
||||||
?? throw new InvalidOperationException("CollectedByUserId is required while auth is unavailable");
|
?? throw new InvalidOperationException("CollectedByUserId is required while auth is unavailable");
|
||||||
|
var collectedBy = request.CollectedByUserId ?? recorderId;
|
||||||
|
var recordedBy = request.EntryMode == EntryMode.DayEndManual && collectedBy != recorderId
|
||||||
|
? recorderId
|
||||||
|
: (Guid?)null;
|
||||||
|
|
||||||
var payment = new Models.Payment
|
var payment = new Models.Payment
|
||||||
{
|
{
|
||||||
FK_InvoiceId = invoiceId,
|
FK_InvoiceId = invoiceId,
|
||||||
Amount = request.Amount,
|
Amount = request.Amount,
|
||||||
Method = request.Method,
|
Method = request.Method,
|
||||||
|
EntryMode = request.EntryMode,
|
||||||
|
ChequeNumber = request.Method == PaymentMethod.Cheque ? request.ChequeNumber : null,
|
||||||
|
ChequeDate = request.Method == PaymentMethod.Cheque ? request.ChequeDate : null,
|
||||||
CollectedAt = DateTime.Now,
|
CollectedAt = DateTime.Now,
|
||||||
CollectedBy = userId
|
CollectedBy = collectedBy,
|
||||||
|
RecordedBy = recordedBy
|
||||||
};
|
};
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
using Distribution.Models;
|
|
||||||
|
|
||||||
namespace Distribution.Features.Ride;
|
|
||||||
|
|
||||||
public interface IRideCancelRequestRepository
|
|
||||||
{
|
|
||||||
Task<RideCancelRequest?> GetByIdAsync(Guid id);
|
|
||||||
Task<RideCancelRequest?> GetByRideIdAsync(Guid rideId);
|
|
||||||
Task<RideCancelRequest?> GetPendingByRideIdAsync(Guid rideId);
|
|
||||||
Task<List<RideCancelRequest>> GetAllAsync(CancelRequestStatus? status, int page, int pageSize);
|
|
||||||
Task<int> GetCountAsync(CancelRequestStatus? status);
|
|
||||||
Task AddAsync(RideCancelRequest request);
|
|
||||||
void Update(RideCancelRequest request);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using Distribution.Data;
|
|
||||||
using Distribution.Models;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace Distribution.Features.Ride;
|
|
||||||
|
|
||||||
public class RideCancelRequestRepository(AppDBContext context) : IRideCancelRequestRepository
|
|
||||||
{
|
|
||||||
public async Task<RideCancelRequest?> GetByIdAsync(Guid id) =>
|
|
||||||
await context.RideCancelRequests.FirstOrDefaultAsync(r => r.Id == id);
|
|
||||||
|
|
||||||
public async Task<RideCancelRequest?> GetByRideIdAsync(Guid rideId) =>
|
|
||||||
await context.RideCancelRequests
|
|
||||||
.Where(r => r.FK_RideId == rideId)
|
|
||||||
.OrderByDescending(r => r.RequestedAt)
|
|
||||||
.FirstOrDefaultAsync();
|
|
||||||
|
|
||||||
public async Task<RideCancelRequest?> GetPendingByRideIdAsync(Guid rideId) =>
|
|
||||||
await context.RideCancelRequests
|
|
||||||
.FirstOrDefaultAsync(r => r.FK_RideId == rideId && r.Status == CancelRequestStatus.Pending);
|
|
||||||
|
|
||||||
public async Task<List<RideCancelRequest>> GetAllAsync(CancelRequestStatus? status, int page, int pageSize) =>
|
|
||||||
await context.RideCancelRequests
|
|
||||||
.Where(r => status == null || r.Status == status)
|
|
||||||
.OrderByDescending(r => r.RequestedAt)
|
|
||||||
.Skip((page - 1) * pageSize)
|
|
||||||
.Take(pageSize)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
public async Task<int> GetCountAsync(CancelRequestStatus? status) =>
|
|
||||||
await context.RideCancelRequests
|
|
||||||
.CountAsync(r => status == null || r.Status == status);
|
|
||||||
|
|
||||||
public async Task AddAsync(RideCancelRequest request) =>
|
|
||||||
await context.RideCancelRequests.AddAsync(request);
|
|
||||||
|
|
||||||
public void Update(RideCancelRequest request) =>
|
|
||||||
context.RideCancelRequests.Update(request);
|
|
||||||
}
|
|
||||||
+3992
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveRideCancelRequest : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "RideCancelRequests");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "RideCancelRequests",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FK_RideId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
ApprovedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
|
||||||
|
ApprovedBy = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
|
Reason = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
RequestedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
RequestedBy = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TypeCode = table.Column<int>(type: "int", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_RideCancelRequests", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_RideCancelRequests_DeliveryRuns_FK_RideId",
|
||||||
|
column: x => x.FK_RideId,
|
||||||
|
principalTable: "DeliveryRuns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "RideCancelRequests",
|
||||||
|
columns: new[] { "Id", "ApprovedAt", "ApprovedBy", "FK_RideId", "Reason", "RequestedAt", "RequestedBy", "Status", "TypeCode" },
|
||||||
|
values: new object[] { new Guid("65000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 30, 9, 15, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000001"), new Guid("31000000-0000-0000-0000-000000000002"), "Reported minor engine issue, resolved after inspection", new DateTime(2026, 8, 30, 9, 10, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000005"), 2, 0 });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_RideCancelRequests_FK_RideId",
|
||||||
|
table: "RideCancelRequests",
|
||||||
|
column: "FK_RideId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4005
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddPaymentChequeAndEntryMode : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "ChequeDate",
|
||||||
|
table: "Payments",
|
||||||
|
type: "datetime(6)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ChequeNumber",
|
||||||
|
table: "Payments",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "EntryMode",
|
||||||
|
table: "Payments",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "RecordedBy",
|
||||||
|
table: "Payments",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true,
|
||||||
|
collation: "ascii_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "Payments",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("47000000-0000-0000-0000-000000000001"),
|
||||||
|
columns: new[] { "ChequeDate", "ChequeNumber", "EntryMode", "RecordedBy" },
|
||||||
|
values: new object[] { null, null, 0, null });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ChequeDate",
|
||||||
|
table: "Payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ChequeNumber",
|
||||||
|
table: "Payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "EntryMode",
|
||||||
|
table: "Payments");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "RecordedBy",
|
||||||
|
table: "Payments");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4082
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Distribution.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class SeedInvoice2AndMorePayments : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "ShopPOs",
|
||||||
|
columns: new[] { "Id", "ConfirmedAt", "ConfirmedBy", "CreatedAt", "FK_StoreId", "FulfilledAt", "InvoiceId", "IsActive", "RaisedById", "Status", "TotalAmount" },
|
||||||
|
values: new object[] { new Guid("33000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 31, 8, 30, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000001"), new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), new Guid("26000000-0000-0000-0000-000000000005"), new DateTime(2026, 8, 31, 13, 0, 0, 0, DateTimeKind.Unspecified), new Guid("34500000-0000-0000-0000-000000000002"), true, new Guid("29000000-0000-0000-0000-000000000003"), 2, 2400m });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "Invoices",
|
||||||
|
columns: new[] { "Id", "CreatedAt", "CreatedBy", "FK_ShopPOId", "FinalizedAt", "FinalizedBy", "IsActive", "PaymentStatus", "PrintedAt", "PrintedBy", "Status", "TotalAmount" },
|
||||||
|
values: new object[] { new Guid("34500000-0000-0000-0000-000000000002"), new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000003"), new Guid("33000000-0000-0000-0000-000000000003"), null, null, true, 1, new DateTime(2026, 8, 31, 9, 15, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000003"), 1, 2400m });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "ShopPOItems",
|
||||||
|
columns: new[] { "Id", "CreatedAt", "FK_ItemId", "FK_ShopPOId", "IsActive", "Quantity", "QuantityFulfilled", "UnitPrice" },
|
||||||
|
values: new object[] { new Guid("34000000-0000-0000-0000-000000000004"), new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified), new Guid("23000000-0000-0000-0000-000000000002"), new Guid("33000000-0000-0000-0000-000000000003"), true, 20, 20, 120m });
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "Payments",
|
||||||
|
columns: new[] { "Id", "Amount", "ChequeDate", "ChequeNumber", "CollectedAt", "CollectedBy", "CreatedAt", "EntryMode", "FK_InvoiceId", "IsActive", "Method", "RecordedBy" },
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("47000000-0000-0000-0000-000000000002"), 1000m, null, null, new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified), 0, new Guid("34500000-0000-0000-0000-000000000002"), true, 0, null },
|
||||||
|
{ new Guid("47000000-0000-0000-0000-000000000003"), 800m, new DateTime(2026, 8, 31, 0, 0, 0, 0, DateTimeKind.Unspecified), "000456", new DateTime(2026, 8, 31, 11, 0, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 31, 18, 0, 0, 0, DateTimeKind.Unspecified), 1, new Guid("34500000-0000-0000-0000-000000000002"), true, 1, new Guid("29000000-0000-0000-0000-000000000001") },
|
||||||
|
{ new Guid("47000000-0000-0000-0000-000000000004"), 300m, null, null, new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), new Guid("29000000-0000-0000-0000-000000000003"), new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified), 0, new Guid("34500000-0000-0000-0000-000000000002"), true, 2, null }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Payments",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("47000000-0000-0000-0000-000000000002"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Payments",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("47000000-0000-0000-0000-000000000003"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Payments",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("47000000-0000-0000-0000-000000000004"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ShopPOItems",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("34000000-0000-0000-0000-000000000004"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "Invoices",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("34500000-0000-0000-0000-000000000002"));
|
||||||
|
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "ShopPOs",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("33000000-0000-0000-0000-000000000003"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -572,6 +572,19 @@ namespace Distribution.Migrations
|
|||||||
PrintedBy = new Guid("29000000-0000-0000-0000-000000000002"),
|
PrintedBy = new Guid("29000000-0000-0000-0000-000000000002"),
|
||||||
Status = 2,
|
Status = 2,
|
||||||
TotalAmount = 3000m
|
TotalAmount = 3000m
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("34500000-0000-0000-0000-000000000002"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
CreatedBy = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"),
|
||||||
|
IsActive = true,
|
||||||
|
PaymentStatus = 1,
|
||||||
|
PrintedAt = new DateTime(2026, 8, 31, 9, 15, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
PrintedBy = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
Status = 1,
|
||||||
|
TotalAmount = 2400m
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -833,6 +846,12 @@ namespace Distribution.Migrations
|
|||||||
b.Property<decimal>("Amount")
|
b.Property<decimal>("Amount")
|
||||||
.HasColumnType("decimal(65,30)");
|
.HasColumnType("decimal(65,30)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ChequeDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("ChequeNumber")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<DateTime>("CollectedAt")
|
b.Property<DateTime>("CollectedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
@@ -842,6 +861,9 @@ namespace Distribution.Migrations
|
|||||||
b.Property<DateTime>("CreatedAt")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("EntryMode")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<Guid>("FK_InvoiceId")
|
b.Property<Guid>("FK_InvoiceId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@@ -851,6 +873,9 @@ namespace Distribution.Migrations
|
|||||||
b.Property<int>("Method")
|
b.Property<int>("Method")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("RecordedBy")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("FK_InvoiceId");
|
b.HasIndex("FK_InvoiceId");
|
||||||
@@ -865,9 +890,49 @@ namespace Distribution.Migrations
|
|||||||
CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified),
|
CollectedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified),
|
||||||
CollectedBy = new Guid("29000000-0000-0000-0000-000000000002"),
|
CollectedBy = new Guid("29000000-0000-0000-0000-000000000002"),
|
||||||
CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified),
|
CreatedAt = new DateTime(2026, 8, 25, 9, 30, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
EntryMode = 0,
|
||||||
FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"),
|
FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000001"),
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
Method = 0
|
Method = 0
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("47000000-0000-0000-0000-000000000002"),
|
||||||
|
Amount = 1000m,
|
||||||
|
CollectedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 20, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
EntryMode = 0,
|
||||||
|
FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"),
|
||||||
|
IsActive = true,
|
||||||
|
Method = 0
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("47000000-0000-0000-0000-000000000003"),
|
||||||
|
Amount = 800m,
|
||||||
|
ChequeDate = new DateTime(2026, 8, 31, 0, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
ChequeNumber = "000456",
|
||||||
|
CollectedAt = new DateTime(2026, 8, 31, 11, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 18, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
EntryMode = 1,
|
||||||
|
FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"),
|
||||||
|
IsActive = true,
|
||||||
|
Method = 1,
|
||||||
|
RecordedBy = new Guid("29000000-0000-0000-0000-000000000001")
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("47000000-0000-0000-0000-000000000004"),
|
||||||
|
Amount = 300m,
|
||||||
|
CollectedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
CollectedBy = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 11, 5, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
EntryMode = 0,
|
||||||
|
FK_InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"),
|
||||||
|
IsActive = true,
|
||||||
|
Method = 2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1407,58 +1472,6 @@ namespace Distribution.Migrations
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.RideCancelRequest", b =>
|
|
||||||
{
|
|
||||||
b.Property<Guid>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("ApprovedAt")
|
|
||||||
.HasColumnType("datetime(6)");
|
|
||||||
|
|
||||||
b.Property<Guid?>("ApprovedBy")
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<Guid>("FK_RideId")
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<string>("Reason")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("longtext");
|
|
||||||
|
|
||||||
b.Property<DateTime>("RequestedAt")
|
|
||||||
.HasColumnType("datetime(6)");
|
|
||||||
|
|
||||||
b.Property<Guid>("RequestedBy")
|
|
||||||
.HasColumnType("char(36)");
|
|
||||||
|
|
||||||
b.Property<int>("Status")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.Property<int>("TypeCode")
|
|
||||||
.HasColumnType("int");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("FK_RideId");
|
|
||||||
|
|
||||||
b.ToTable("RideCancelRequests");
|
|
||||||
|
|
||||||
b.HasData(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
Id = new Guid("65000000-0000-0000-0000-000000000001"),
|
|
||||||
ApprovedAt = new DateTime(2026, 8, 30, 9, 15, 0, 0, DateTimeKind.Unspecified),
|
|
||||||
ApprovedBy = new Guid("29000000-0000-0000-0000-000000000001"),
|
|
||||||
FK_RideId = new Guid("31000000-0000-0000-0000-000000000002"),
|
|
||||||
Reason = "Reported minor engine issue, resolved after inspection",
|
|
||||||
RequestedAt = new DateTime(2026, 8, 30, 9, 10, 0, 0, DateTimeKind.Unspecified),
|
|
||||||
RequestedBy = new Guid("29000000-0000-0000-0000-000000000005"),
|
|
||||||
Status = 2,
|
|
||||||
TypeCode = 0
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.Role", b =>
|
modelBuilder.Entity("Distribution.Models.Role", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -2104,6 +2117,20 @@ namespace Distribution.Migrations
|
|||||||
RaisedById = new Guid("29000000-0000-0000-0000-000000000003"),
|
RaisedById = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
Status = 0,
|
Status = 0,
|
||||||
TotalAmount = 1200m
|
TotalAmount = 1200m
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("33000000-0000-0000-0000-000000000003"),
|
||||||
|
ConfirmedAt = new DateTime(2026, 8, 31, 8, 30, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
ConfirmedBy = new Guid("29000000-0000-0000-0000-000000000001"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
FK_StoreId = new Guid("26000000-0000-0000-0000-000000000005"),
|
||||||
|
FulfilledAt = new DateTime(2026, 8, 31, 13, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
InvoiceId = new Guid("34500000-0000-0000-0000-000000000002"),
|
||||||
|
IsActive = true,
|
||||||
|
RaisedById = new Guid("29000000-0000-0000-0000-000000000003"),
|
||||||
|
Status = 2,
|
||||||
|
TotalAmount = 2400m
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2175,6 +2202,17 @@ namespace Distribution.Migrations
|
|||||||
Quantity = 10,
|
Quantity = 10,
|
||||||
QuantityFulfilled = 0,
|
QuantityFulfilled = 0,
|
||||||
UnitPrice = 120m
|
UnitPrice = 120m
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("34000000-0000-0000-0000-000000000004"),
|
||||||
|
CreatedAt = new DateTime(2026, 8, 31, 9, 0, 0, 0, DateTimeKind.Unspecified),
|
||||||
|
FK_ItemId = new Guid("23000000-0000-0000-0000-000000000002"),
|
||||||
|
FK_ShopPOId = new Guid("33000000-0000-0000-0000-000000000003"),
|
||||||
|
IsActive = true,
|
||||||
|
Quantity = 20,
|
||||||
|
QuantityFulfilled = 20,
|
||||||
|
UnitPrice = 120m
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3553,17 +3591,6 @@ namespace Distribution.Migrations
|
|||||||
b.Navigation("Region");
|
b.Navigation("Region");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.RideCancelRequest", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Distribution.Models.DeliveryRun", "Ride")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("FK_RideId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Ride");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Distribution.Models.RolePermission", b =>
|
modelBuilder.Entity("Distribution.Models.RolePermission", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Distribution.Models.Permission", "Permission")
|
b.HasOne("Distribution.Models.Permission", "Permission")
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ namespace Distribution.Models
|
|||||||
|
|
||||||
public enum CancelRequestType { VehicleBreakdown, DriverUnavailable, RouteIssue, Other }
|
public enum CancelRequestType { VehicleBreakdown, DriverUnavailable, RouteIssue, Other }
|
||||||
|
|
||||||
public enum CancelRequestStatus { Pending, Approved, Rejected }
|
|
||||||
|
|
||||||
public enum PurchaseOrderStatus { Issued, PartiallyReceived, Received, Closed, Cancelled }
|
public enum PurchaseOrderStatus { Issued, PartiallyReceived, Received, Closed, Cancelled }
|
||||||
|
|
||||||
public enum ShopPOStatus { Pending, PartiallyFulfilled, Fulfilled, Cancelled }
|
public enum ShopPOStatus { Pending, PartiallyFulfilled, Fulfilled, Cancelled }
|
||||||
@@ -24,7 +22,9 @@ namespace Distribution.Models
|
|||||||
|
|
||||||
public enum PaymentStatus { Pending, PartiallyPaid, Paid }
|
public enum PaymentStatus { Pending, PartiallyPaid, Paid }
|
||||||
|
|
||||||
public enum PaymentMethod { Cash, Credit }
|
public enum PaymentMethod { Cash, Cheque, Credit }
|
||||||
|
|
||||||
|
public enum EntryMode { RealTime, DayEndManual }
|
||||||
|
|
||||||
public enum DeliveryTaskStatus { Pending, Delivered, PartiallyDelivered, Failed }
|
public enum DeliveryTaskStatus { Pending, Delivered, PartiallyDelivered, Failed }
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ namespace Distribution.Models
|
|||||||
|
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public PaymentMethod Method { get; set; }
|
public PaymentMethod Method { get; set; }
|
||||||
|
public EntryMode EntryMode { get; set; } = EntryMode.RealTime;
|
||||||
|
public string? ChequeNumber { get; set; }
|
||||||
|
public DateTime? ChequeDate { get; set; }
|
||||||
public DateTime CollectedAt { get; set; }
|
public DateTime CollectedAt { get; set; }
|
||||||
public Guid CollectedBy { get; set; }
|
public Guid CollectedBy { get; set; }
|
||||||
|
public Guid? RecordedBy { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace Distribution.Models
|
|
||||||
{
|
|
||||||
public class RideCancelRequest
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
|
||||||
|
|
||||||
public Guid FK_RideId { get; set; }
|
|
||||||
[ForeignKey("FK_RideId")]
|
|
||||||
public DeliveryRun Ride { get; set; }
|
|
||||||
|
|
||||||
public CancelRequestType TypeCode { get; set; }
|
|
||||||
public string Reason { get; set; }
|
|
||||||
|
|
||||||
public Guid RequestedBy { get; set; }
|
|
||||||
public DateTime RequestedAt { get; set; } = DateTime.Now;
|
|
||||||
|
|
||||||
public CancelRequestStatus Status { get; set; } = CancelRequestStatus.Pending;
|
|
||||||
public Guid? ApprovedBy { get; set; }
|
|
||||||
public DateTime? ApprovedAt { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,36 @@
|
|||||||
# [Refactor] Ride Lifecycle — Route-Free, Type-Free Rides
|
# [Refactor] Ride Lifecycle — Route-Free, Type-Free Rides
|
||||||
|
|
||||||
**Status**: Proposed (design only — no code changes yet)
|
**Status**: Mostly implemented — see [Implementation Status](#implementation-status-2026-09-17)
|
||||||
**Refactors**: [../03_ride_lifecycle.md](../03_ride_lifecycle.md) (kept as-is, this is the target design)
|
**Refactors**: [../03_ride_lifecycle.md](../03_ride_lifecycle.md) (kept as-is, this is the target design)
|
||||||
**Depends On**: 02 — Stock Ledger
|
**Depends On**: 02 — Stock Ledger
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Implementation Status (2026-09-17)
|
||||||
|
|
||||||
|
**Done**, verified against `Models/DeliveryRun.cs`, `Features/Ride/*`:
|
||||||
|
- `RideStatus` collapsed to `Open`/`Cancelled`/`Closed` — no Draft/Active stage anywhere.
|
||||||
|
- `FK_RouteId`, `RideType`, `ActivatedAt`, `FK_SourceWarehouseId` are all gone from `DeliveryRun`.
|
||||||
|
- `POST /api/rides` creates directly into `Open` with `StartedAt` set; no `/open`/`/activate` routes.
|
||||||
|
- `POST /api/rides/{id}/cancel` cancels an `Open` ride directly — no approval step, no
|
||||||
|
`CancelRequested` stage.
|
||||||
|
- Driver compliance check (`CheckDriverComplianceAsync`) and the "vehicle can't have two Open rides"
|
||||||
|
guard are both wired into `RideService.CreateAsync`.
|
||||||
|
- Dead `RideCancelRequest` model/repository/table removed (2026-09-17) — see Migration Notes.
|
||||||
|
|
||||||
|
**Not done / diverges from this doc:**
|
||||||
|
1. **`GET /api/rides/breakdown-assist/{rideId}`** — not implemented at all (no route in
|
||||||
|
`RideController.cs`), not even the trimmed "vehicle stock levels only" version this doc describes.
|
||||||
|
2. **Impact item 1 (delivery task generation) confirmed still broken, honestly**:
|
||||||
|
`DeliveryTaskService.GenerateAsync` is a one-line `throw new NotImplementedException("... route-
|
||||||
|
based dispatch not yet implemented.")` — matches this doc's prediction exactly, not yet designed.
|
||||||
|
3. **Impact item 4 (stock reconciliation at Close/Cancel without a source warehouse)** — still
|
||||||
|
unaddressed; `CloseAsync`/`CancelAsync` do not touch `StockBalance` at all.
|
||||||
|
4. **Impact item 2 (loading sheet auto-population)** — resolved differently than either this doc or
|
||||||
|
`loading_event.md` predicted; see that document's own Implementation Status section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Ride should not require a `Route`, should not carry a `RideType`, should not be gated by a warehouse's `Type`, and should not even pin down a source warehouse on the ride record itself. Per direction from the business owner, the ride record carries only what a real dispatch needs to track — **"a ride only carries vehicle, driver assign, and meter reading from start to end."** Everything route-shaped (which stores get visited, which orders/items travel on this trip) and everything warehouse-shaped (where stock loads from/returns to) moves out of the Ride entity's responsibility entirely; neither is replaced by another matching mechanism here.
|
Ride should not require a `Route`, should not carry a `RideType`, should not be gated by a warehouse's `Type`, and should not even pin down a source warehouse on the ride record itself. Per direction from the business owner, the ride record carries only what a real dispatch needs to track — **"a ride only carries vehicle, driver assign, and meter reading from start to end."** Everything route-shaped (which stores get visited, which orders/items travel on this trip) and everything warehouse-shaped (where stock loads from/returns to) moves out of the Ride entity's responsibility entirely; neither is replaced by another matching mechanism here.
|
||||||
@@ -215,9 +240,10 @@ Not part of this docs-only pass, but captured for the eventual implementation st
|
|||||||
`DeliveryRun`/`Ride` and their Fluent/EF configuration. `Status` stays, re-typed to the narrowed
|
`DeliveryRun`/`Ride` and their Fluent/EF configuration. `Status` stays, re-typed to the narrowed
|
||||||
`RideStatus` (`Open`/`Cancelled`/`Closed`). Add `CancelTypeCode`, `CancelReason`, `CancelledAt`
|
`RideStatus` (`Open`/`Cancelled`/`Closed`). Add `CancelTypeCode`, `CancelReason`, `CancelledAt`
|
||||||
columns to `DeliveryRun`.
|
columns to `DeliveryRun`.
|
||||||
- Drop the `RideCancelRequest` table, its Fluent/EF configuration, and
|
- ~~Drop the `RideCancelRequest` table, its Fluent/EF configuration, and
|
||||||
`RideCancelRequestRepository`/`IRideCancelRequestRepository` — the request/approve/reject flow is
|
`RideCancelRequestRepository`/`IRideCancelRequestRepository`~~ — **Done (2026-09-17)**: model,
|
||||||
gone.
|
repository, `DbSet`, and seed data removed; `CancelRequestStatus` enum removed as unused;
|
||||||
|
migration `RemoveRideCancelRequest` applied, dropping the table.
|
||||||
- Remove `RouteId`, `RideType`, and `SourceWarehouseId`/`SourceWarehouseName` from
|
- Remove `RouteId`, `RideType`, and `SourceWarehouseId`/`SourceWarehouseName` from
|
||||||
`CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. Remove the
|
`CreateRideRequest`, `RideResponse`, and `BreakdownAssistRideResponse` in `RideDTOs.cs`. Remove the
|
||||||
open-orders list from `BreakdownAssistResponse`. Remove the `RaiseCancelRequest`/cancel-request
|
open-orders list from `BreakdownAssistResponse`. Remove the `RaiseCancelRequest`/cancel-request
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -25,11 +26,23 @@ import { toast } from '@/components/ui/toast'
|
|||||||
import { payments as paymentsApi, getApiError } from '@/lib/api/index'
|
import { payments as paymentsApi, getApiError } from '@/lib/api/index'
|
||||||
import type { InvoiceResponse } from '@/types/api'
|
import type { InvoiceResponse } from '@/types/api'
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z
|
||||||
invoiceId: z.string().min(1, 'Invoice is required'),
|
.object({
|
||||||
amount: z.number().min(0.01, 'Amount must be greater than 0'),
|
invoiceId: z.string().min(1, 'Invoice is required'),
|
||||||
method: z.enum(['Cash', 'Credit']),
|
amount: z.number().min(0.01, 'Amount must be greater than 0'),
|
||||||
})
|
method: z.enum(['Cash', 'Cheque', 'Credit']),
|
||||||
|
entryMode: z.enum(['RealTime', 'DayEndManual']),
|
||||||
|
chequeNumber: z.string().optional(),
|
||||||
|
chequeDate: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine((data) => data.method !== 'Cheque' || !!data.chequeNumber, {
|
||||||
|
message: 'Cheque number is required for cheque payments',
|
||||||
|
path: ['chequeNumber'],
|
||||||
|
})
|
||||||
|
.refine((data) => data.method !== 'Cheque' || !!data.chequeDate, {
|
||||||
|
message: 'Cheque date is required for cheque payments',
|
||||||
|
path: ['chequeDate'],
|
||||||
|
})
|
||||||
|
|
||||||
type FormValues = z.infer<typeof schema>
|
type FormValues = z.infer<typeof schema>
|
||||||
|
|
||||||
@@ -50,18 +63,35 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
|
|||||||
invoiceId: '',
|
invoiceId: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
method: 'Cash',
|
method: 'Cash',
|
||||||
|
entryMode: 'RealTime',
|
||||||
|
chequeNumber: '',
|
||||||
|
chequeDate: '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const method = form.watch('method')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
form.reset({ invoiceId: '', amount: 0, method: 'Cash' })
|
form.reset({
|
||||||
|
invoiceId: '',
|
||||||
|
amount: 0,
|
||||||
|
method: 'Cash',
|
||||||
|
entryMode: 'RealTime',
|
||||||
|
chequeNumber: '',
|
||||||
|
chequeDate: '',
|
||||||
|
})
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
async function onSubmit(values: FormValues) {
|
async function onSubmit(values: FormValues) {
|
||||||
const { invoiceId, ...rest } = values
|
const { invoiceId, method, chequeNumber, chequeDate, ...rest } = values
|
||||||
try {
|
try {
|
||||||
await paymentsApi.create(invoiceId, rest)
|
await paymentsApi.create(invoiceId, {
|
||||||
|
...rest,
|
||||||
|
method,
|
||||||
|
chequeNumber: method === 'Cheque' ? chequeNumber : undefined,
|
||||||
|
chequeDate: method === 'Cheque' ? chequeDate : undefined,
|
||||||
|
})
|
||||||
toast.add({ title: 'Payment recorded', type: 'success' })
|
toast.add({ title: 'Payment recorded', type: 'success' })
|
||||||
onSuccess()
|
onSuccess()
|
||||||
onClose()
|
onClose()
|
||||||
@@ -77,7 +107,7 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
|
|||||||
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Record Payment</DialogTitle>
|
<DialogTitle>Record Payment</DialogTitle>
|
||||||
<DialogDescription>Record a cash or credit payment against an invoice.</DialogDescription>
|
<DialogDescription>Record a cash, cheque, or credit payment against an invoice.</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||||
@@ -137,11 +167,71 @@ export function PaymentSheet({ open, onClose, onSuccess, invoices }: PaymentShee
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="Cash">Cash</SelectItem>
|
<SelectItem value="Cash">Cash</SelectItem>
|
||||||
|
<SelectItem value="Cheque">Cheque</SelectItem>
|
||||||
<SelectItem value="Credit">Credit</SelectItem>
|
<SelectItem value="Credit">Credit</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{method === 'Cheque' && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chequeNumber"
|
||||||
|
label="Cheque Number"
|
||||||
|
required
|
||||||
|
render={({ field, invalid }) => (
|
||||||
|
<Input
|
||||||
|
id="chequeNumber"
|
||||||
|
name={field.name}
|
||||||
|
ref={field.ref}
|
||||||
|
value={(field.value as string) ?? ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
placeholder="e.g. 000123"
|
||||||
|
aria-invalid={invalid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="chequeDate"
|
||||||
|
label="Cheque Date"
|
||||||
|
required
|
||||||
|
render={({ field, invalid }) => (
|
||||||
|
<Input
|
||||||
|
id="chequeDate"
|
||||||
|
name={field.name}
|
||||||
|
ref={field.ref}
|
||||||
|
type="date"
|
||||||
|
value={(field.value as string) ?? ''}
|
||||||
|
onChange={field.onChange}
|
||||||
|
onBlur={field.onBlur}
|
||||||
|
aria-invalid={invalid}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="entryMode"
|
||||||
|
label="Entry Mode"
|
||||||
|
required
|
||||||
|
render={({ field }) => (
|
||||||
|
<ToggleGroup
|
||||||
|
variant="outline"
|
||||||
|
value={[field.value as string]}
|
||||||
|
onValueChange={(value: string[]) => value[0] && field.onChange(value[0])}
|
||||||
|
>
|
||||||
|
<ToggleGroupItem value="RealTime">Real-time</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="DayEndManual">Day-end entry</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -7,13 +7,19 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import type { PaymentResponse, PaymentMethod } from '@/types/api'
|
import type { PaymentResponse, PaymentMethod, EntryMode } from '@/types/api'
|
||||||
|
|
||||||
const PAYMENT_METHOD_VARIANT: Record<PaymentMethod, 'default' | 'secondary'> = {
|
const PAYMENT_METHOD_VARIANT: Record<PaymentMethod, 'default' | 'secondary' | 'outline'> = {
|
||||||
Cash: 'default',
|
Cash: 'default',
|
||||||
|
Cheque: 'outline',
|
||||||
Credit: 'secondary',
|
Credit: 'secondary',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENTRY_MODE_LABEL: Record<EntryMode, string> = {
|
||||||
|
RealTime: 'Real-time',
|
||||||
|
DayEndManual: 'Day-end entry',
|
||||||
|
}
|
||||||
|
|
||||||
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
|
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
|
||||||
const truncId = (id: string) => id.substring(0, 8)
|
const truncId = (id: string) => id.substring(0, 8)
|
||||||
|
|
||||||
@@ -22,6 +28,9 @@ const fmtDateTime = (v: string) =>
|
|||||||
day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit',
|
day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fmtDate = (v: string) =>
|
||||||
|
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||||
|
|
||||||
interface PaymentViewSheetProps {
|
interface PaymentViewSheetProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -53,8 +62,18 @@ export function PaymentViewSheet({ open, onClose, payment, invoiceName }: Paymen
|
|||||||
<Row label="Invoice" value={invoiceName ?? <span className="font-mono">{truncId(payment.invoiceId)}</span>} />
|
<Row label="Invoice" value={invoiceName ?? <span className="font-mono">{truncId(payment.invoiceId)}</span>} />
|
||||||
<Row label="Amount" value={fmtAmount(payment.amount)} />
|
<Row label="Amount" value={fmtAmount(payment.amount)} />
|
||||||
<Row label="Method" value={<Badge variant={PAYMENT_METHOD_VARIANT[payment.method]}>{payment.method}</Badge>} />
|
<Row label="Method" value={<Badge variant={PAYMENT_METHOD_VARIANT[payment.method]}>{payment.method}</Badge>} />
|
||||||
|
{payment.method === 'Cheque' && (
|
||||||
|
<>
|
||||||
|
<Row label="Cheque Number" value={payment.chequeNumber ?? '—'} />
|
||||||
|
<Row label="Cheque Date" value={payment.chequeDate ? fmtDate(payment.chequeDate) : '—'} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Row label="Entry Mode" value={<Badge variant="secondary">{ENTRY_MODE_LABEL[payment.entryMode]}</Badge>} />
|
||||||
<Row label="Collected At" value={fmtDateTime(payment.collectedAt)} />
|
<Row label="Collected At" value={fmtDateTime(payment.collectedAt)} />
|
||||||
<Row label="Collected By" value={<span className="font-mono">{truncId(payment.collectedBy)}</span>} />
|
<Row label="Collected By" value={<span className="font-mono">{truncId(payment.collectedBy)}</span>} />
|
||||||
|
{payment.recordedBy && (
|
||||||
|
<Row label="Recorded By" value={<span className="font-mono">{truncId(payment.recordedBy)}</span>} />
|
||||||
|
)}
|
||||||
<Row label="Created At" value={fmtDateTime(payment.createdAt)} />
|
<Row label="Created At" value={fmtDateTime(payment.createdAt)} />
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
import { invoices as invoicesApi, payments as paymentsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
import { invoices as invoicesApi, payments as paymentsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||||
import type { PaymentResponse, PaymentMethod, InvoiceResponse } from '@/types/api'
|
import type { PaymentResponse, PaymentMethod, EntryMode, InvoiceResponse } from '@/types/api'
|
||||||
|
|
||||||
import { PaymentSheet } from './payment-sheet'
|
import { PaymentSheet } from './payment-sheet'
|
||||||
import { PaymentViewSheet } from './payment-view-sheet'
|
import { PaymentViewSheet } from './payment-view-sheet'
|
||||||
@@ -22,11 +22,17 @@ const fmtDate = (v: string) =>
|
|||||||
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
|
const fmtAmount = (n: number) => `Rs. ${n.toLocaleString()}`
|
||||||
const truncId = (id: string) => id.substring(0, 8)
|
const truncId = (id: string) => id.substring(0, 8)
|
||||||
|
|
||||||
const PAYMENT_METHOD_VARIANT: Record<PaymentMethod, 'default' | 'secondary'> = {
|
const PAYMENT_METHOD_VARIANT: Record<PaymentMethod, 'default' | 'secondary' | 'outline'> = {
|
||||||
Cash: 'default',
|
Cash: 'default',
|
||||||
|
Cheque: 'outline',
|
||||||
Credit: 'secondary',
|
Credit: 'secondary',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENTRY_MODE_LABEL: Record<EntryMode, string> = {
|
||||||
|
RealTime: 'Real-time',
|
||||||
|
DayEndManual: 'Day-end entry',
|
||||||
|
}
|
||||||
|
|
||||||
export function PaymentsClient() {
|
export function PaymentsClient() {
|
||||||
const [invoicesList, setInvoicesList] = useState<InvoiceResponse[]>([])
|
const [invoicesList, setInvoicesList] = useState<InvoiceResponse[]>([])
|
||||||
const [selectedInvoiceId, setSelectedInvoiceId] = useState('')
|
const [selectedInvoiceId, setSelectedInvoiceId] = useState('')
|
||||||
@@ -119,6 +125,15 @@ export function PaymentsClient() {
|
|||||||
<Badge variant={PAYMENT_METHOD_VARIANT[row.method]}>{row.method}</Badge>
|
<Badge variant={PAYMENT_METHOD_VARIANT[row.method]}>{row.method}</Badge>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'entryMode',
|
||||||
|
title: 'Entry Mode',
|
||||||
|
fields: ['entryMode'],
|
||||||
|
width: 130,
|
||||||
|
cellRender: (row) => (
|
||||||
|
<Badge variant="secondary">{ENTRY_MODE_LABEL[row.entryMode]}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'collectedAt',
|
key: 'collectedAt',
|
||||||
title: 'Collected At',
|
title: 'Collected At',
|
||||||
@@ -143,7 +158,7 @@ export function PaymentsClient() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-semibold text-foreground">Payments</h1>
|
<h1 className="text-2xl font-semibold text-foreground">Payments</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
Cash and credit payments collected against invoices
|
Cash, cheque, and credit payments collected against invoices
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => setSheetOpen(true)}>
|
<Button onClick={() => setSheetOpen(true)}>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
||||||
|
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
|
||||||
|
import { type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "cn"
|
||||||
|
|
||||||
|
import { toggleVariants } from "@/components/ui/toggle"
|
||||||
|
|
||||||
|
const ToggleGroupContext = React.createContext<
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
|
}
|
||||||
|
>({
|
||||||
|
size: "default",
|
||||||
|
variant: "default",
|
||||||
|
spacing: 2,
|
||||||
|
orientation: "horizontal",
|
||||||
|
})
|
||||||
|
|
||||||
|
function ToggleGroup({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
spacing = 2,
|
||||||
|
orientation = "horizontal",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ToggleGroupPrimitive.Props &
|
||||||
|
VariantProps<typeof toggleVariants> & {
|
||||||
|
spacing?: number
|
||||||
|
orientation?: "horizontal" | "vertical"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ToggleGroupPrimitive
|
||||||
|
data-slot="toggle-group"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
data-spacing={spacing}
|
||||||
|
data-orientation={orientation}
|
||||||
|
style={{ "--gap": spacing } as React.CSSProperties}
|
||||||
|
className={cn(
|
||||||
|
"group/toggle-group flex w-fit flex-row items-center gap-[--spacing(var(--gap))] data-[spacing=0]:data-[variant=outline]:rounded-2xl data-vertical:flex-col data-vertical:items-stretch",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ToggleGroupContext.Provider
|
||||||
|
value={{ variant, size, spacing, orientation }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ToggleGroupContext.Provider>
|
||||||
|
</ToggleGroupPrimitive>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleGroupItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
||||||
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TogglePrimitive
|
||||||
|
data-slot="toggle-group-item"
|
||||||
|
data-variant={context.variant || variant}
|
||||||
|
data-size={context.size || size}
|
||||||
|
data-spacing={context.spacing}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 group-data-[spacing=0]/toggle-group:rounded-none group-data-[spacing=0]/toggle-group:px-2 group-data-[spacing=0]/toggle-group:shadow-none focus:z-10 focus-visible:z-10 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-end]:pr-1.5 group-data-[spacing=0]/toggle-group:has-data-[icon=inline-start]:pl-1.5 group-data-horizontal/toggle-group:data-[spacing=0]:first:rounded-l-2xl group-data-vertical/toggle-group:data-[spacing=0]:first:rounded-t-2xl group-data-horizontal/toggle-group:data-[spacing=0]:last:rounded-r-2xl group-data-vertical/toggle-group:data-[spacing=0]:last:rounded-b-2xl data-[state=on]:bg-muted group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:border-l-0 group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:border-t-0 group-data-horizontal/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-l group-data-vertical/toggle-group:data-[spacing=0]:data-[variant=outline]:first:border-t",
|
||||||
|
toggleVariants({
|
||||||
|
variant: context.variant || variant,
|
||||||
|
size: context.size || size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</TogglePrimitive>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "cn"
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"group/toggle inline-flex items-center justify-center gap-1 rounded-2xl text-sm font-medium whitespace-nowrap transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline: "border border-input bg-transparent hover:bg-muted",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
sm: "h-7 min-w-7 px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5",
|
||||||
|
lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Toggle({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
|
||||||
|
return (
|
||||||
|
<TogglePrimitive
|
||||||
|
data-slot="toggle"
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
PaymentResponse,
|
PaymentResponse,
|
||||||
PaymentMethod,
|
PaymentMethod,
|
||||||
|
EntryMode,
|
||||||
ShopPOStatus,
|
ShopPOStatus,
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
|
||||||
@@ -87,6 +88,9 @@ export const invoices = {
|
|||||||
export interface PaymentBody {
|
export interface PaymentBody {
|
||||||
amount: number
|
amount: number
|
||||||
method: PaymentMethod
|
method: PaymentMethod
|
||||||
|
entryMode?: EntryMode
|
||||||
|
chequeNumber?: string | null
|
||||||
|
chequeDate?: string | null
|
||||||
collectedByUserId?: string | null
|
collectedByUserId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+13
@@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.19.0",
|
"axios": "^1.19.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cn": "^0.3.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -3749,6 +3750,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cn": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cn/-/cn-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-qRoU0ARrZD3Bbj1wKFftEKxPjSnzTEdFkf22NVhW+zU9D/wajhoP+qUZ5DChhhSOBCH1onV12hrmGqfu1+5Z2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"cn": "bin/cn.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/code-block-writer": {
|
"node_modules/code-block-writer": {
|
||||||
"version": "13.0.3",
|
"version": "13.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"axios": "^1.19.0",
|
"axios": "^1.19.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cn": "^0.3.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export type ShopPOStatus = 'Pending' | 'PartiallyFulfilled' | 'Fulfilled' | 'Can
|
|||||||
export type InvoiceStatus = 'Draft' | 'Printed' | 'Finalized'
|
export type InvoiceStatus = 'Draft' | 'Printed' | 'Finalized'
|
||||||
export type InvoiceCreationMode = 'ModeA' | 'ModeB'
|
export type InvoiceCreationMode = 'ModeA' | 'ModeB'
|
||||||
export type PaymentStatus = 'Pending' | 'PartiallyPaid' | 'Paid'
|
export type PaymentStatus = 'Pending' | 'PartiallyPaid' | 'Paid'
|
||||||
export type PaymentMethod = 'Cash' | 'Credit'
|
export type PaymentMethod = 'Cash' | 'Cheque' | 'Credit'
|
||||||
|
export type EntryMode = 'RealTime' | 'DayEndManual'
|
||||||
export type DeliveryTaskStatus = 'Pending' | 'Delivered' | 'PartiallyDelivered' | 'Failed'
|
export type DeliveryTaskStatus = 'Pending' | 'Delivered' | 'PartiallyDelivered' | 'Failed'
|
||||||
export type VehicleType = 'Car' | 'Truck' | 'Van' | 'Motorcycle' | 'Bicycle'
|
export type VehicleType = 'Car' | 'Truck' | 'Van' | 'Motorcycle' | 'Bicycle'
|
||||||
export type WarehouseType =
|
export type WarehouseType =
|
||||||
@@ -666,8 +667,12 @@ export interface PaymentResponse {
|
|||||||
invoiceId: string
|
invoiceId: string
|
||||||
amount: number
|
amount: number
|
||||||
method: PaymentMethod
|
method: PaymentMethod
|
||||||
|
entryMode: EntryMode
|
||||||
|
chequeNumber: string | null
|
||||||
|
chequeDate: string | null
|
||||||
collectedAt: string
|
collectedAt: string
|
||||||
collectedBy: string
|
collectedBy: string
|
||||||
|
recordedBy: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user