Added legal pages, contact and abut us
continuous-integration/drone/pr Build is passing

Redesigned account, checkout
Added stock management design elements
This commit is contained in:
Khwezi Mngoma
2026-06-16 23:32:44 +02:00
parent 5d614d2a94
commit 8d2efbeb4a
18 changed files with 1642 additions and 555 deletions
@@ -5,6 +5,14 @@ using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Payments;
using LiteCharms.Features.MidrandBooks.Payments.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Globalization;
using LiteCharms;
using Microsoft.AspNetCore.Components.Authorization;
namespace MidrandBookshop.Components.Pages;
@@ -32,6 +40,13 @@ public partial class Checkout()
public string? OrderNotes { get; set; }
private Dictionary<string, string> CheckoutPayload { get; set; } = [];
// Tracks available quantities indexed by Price ID
protected Dictionary<long, int> AvailableStockMap { get; set; } = [];
// Quick validation flag to evaluate checkout block state
protected bool HasStockExceptions => ShoppingCart.Items.Any(item =>
AvailableStockMap.TryGetValue(item.Price!.Id, out var count) && count <= 0);
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
@@ -41,60 +56,91 @@ public partial class Checkout()
CartService.OnCartChanged += CartService_OnCartChanged;
if (CartService.ShoppingCart.Items.Count == 0)
{
await CartService.LoadCartFromStorageAsync();
}
await RefreshStockValidationAsync();
}
private async void CartService_OnCartChanged() => await InvokeAsync(StateHasChanged);
private async void CartService_OnCartChanged()
{
await RefreshStockValidationAsync();
await InvokeAsync(StateHasChanged);
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e) => StateHasChanged();
private async Task RefreshStockValidationAsync()
{
AvailableStockMap.Clear();
foreach (var item in ShoppingCart.Items)
{
if (item.Price is not null)
{
// Mapped fallback default (set to 0 for specific keys to test stock warnings instantly)
// In production: pull from your inventory system:
// var stockCheck = await BooksService.GetStockLevelAsync(item.Price.Id);
int liveStockAvailable = 1;
AvailableStockMap[item.Price.Id] = liveStockAvailable;
}
}
}
private async Task ChangeQuantity(CartItem item, int delta)
{
var peekQuantity = item.Quantity + delta;
if (peekQuantity < 1) return;
CartService.UpdateQuantity(item.Price!.Id, delta);
// Block internal counters exceeding live available thresholds
if (AvailableStockMap.TryGetValue(item.Price!.Id, out var maxAvailable) && peekQuantity > maxAvailable)
{
ToastService.ShowWarning($"Cannot exceed remaining stock limit ({maxAvailable} available).", "Stock Limit");
return;
}
CartService.UpdateQuantity(item.Price!.Id, delta);
await CartService.SaveCartToStorageAsync();
}
private async Task RemoveFromCart(CartItem item)
{
CartService.RemoveOneItem(item.Price!.Id);
await CartService.SaveCartToStorageAsync();
}
private async Task PayNow(MouseEventArgs args)
{
// Fail-safe protection boundary check
if (HasStockExceptions)
{
ToastService.ShowError("Your order cannot contain items that are out of stock.", "Inventory Issue");
return;
}
if (IsProcessing)
{
ToastService.ShowWarning("Please wait, completing your payment", "Busy...");
return;
}
try
{
IsProcessing = true;
StateHasChanged();
Result<long> orderResult;
var customerId = (long)ShoppingCart.CustomerId!;
if (!ShoppingCart.OrderId.HasValue)
{
CreateOrder request = new(ShoppingCart.TotalAmount, null);
orderResult = await OrderService.CreateOrderAsync(customerId, request, CancellationToken);
ShoppingCart.OrderId = orderResult.Value;
}
List<CreateOrderItem> orderItems = [];
var orderId = (long)ShoppingCart.OrderId;
await OrderService.ClearOrderItemsAsync(orderId, CancellationToken);
@@ -102,7 +148,6 @@ public partial class Checkout()
foreach (var item in ShoppingCart.Items)
{
var bookRequest = await BooksService.GetBookByProductIdAsync(item.Price!.Id, CancellationToken);
if (bookRequest.IsSuccess)
{
var orderItem = new CreateOrderItem(bookRequest.Value.Id, item.Price.Id, item.Quantity);
@@ -114,20 +159,17 @@ public partial class Checkout()
var paymentGen = await PaymentService.CreatePaymentAsync(ShoppingCart.TotalAmount, orderId, orderHash, CancellationToken);
long paymentId = 0;
if (paymentGen.IsSuccess) paymentId = paymentGen.Value;
if (paymentGen.IsFailed)
{
var paymentFetch = await PaymentService.GetOrderPaymentAsync(orderId, CancellationToken);
if (paymentFetch.IsFailed)
{
ToastService.ShowError("Failed to get fetch your previously made payment", "Payment Check");
ToastService.ShowError("Failed to fetch your previously made payment", "Payment Check");
IsProcessing = false;
return;
}
paymentId = paymentFetch.Value.Id;
}
@@ -162,15 +204,13 @@ public partial class Checkout()
CheckoutPayload.Add("signature", signature);
StateHasChanged();
await JSRuntime.InvokeVoidAsync("eval", "document.getElementById('payfastForm').submit();");
}
catch (Exception ex)
{
ToastService.ShowError($"Failed to perform checkout: {ex.Message}", "Checkout");
IsProcessing = false;
StateHasChanged();
}
}
}
}