This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>
|
||||||
<span>Order History</span>
|
<span>Order History</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="nav-link text-start d-flex align-items-center gap-2" data-bs-toggle="pill" data-bs-target="#shipping" role="tab">
|
<button class="nav-link text-start d-flex align-items-center gap-2" data-bs-toggle="pill" data-bs-target="#shipping" role="tab">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||||||
<span>Shipping Address</span>
|
<span>Shipping Address</span>
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
<div class="tab-content account-panels-deck">
|
<div class="tab-content account-panels-deck">
|
||||||
|
|
||||||
<div class="tab-pane fade show active" id="orders" role="tabpanel">
|
<div class="tab-pane fade show active" id="orders" role="tabpanel">
|
||||||
<div class="panel-card-wrapper mb-4">
|
<div class="tab-panel-body">
|
||||||
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Order History</h5>
|
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Order History</h5>
|
||||||
|
|
||||||
@if (orderHistory == null || !orderHistory.Any())
|
@if (orderHistory == null || !orderHistory.Any())
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
@foreach (var order in orderHistory)
|
@foreach (var order in orderHistory)
|
||||||
{
|
{
|
||||||
<div class="premium-order-card p-4 border rounded-3 bg-white shadow-sm">
|
<div class="premium-order-card p-4 border rounded-3 bg-white shadow-sm">
|
||||||
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 pb-3 border-b-dashed mb-3">
|
<div class="d-flex justify-content-between align-items-start flex-wrap gap-3 pb-3 border-bottom border-light mb-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="font-monospace text-dark fw-bold d-block h6 mb-1">@order.OrderId</span>
|
<span class="font-monospace text-dark fw-bold d-block h6 mb-1">@order.OrderId</span>
|
||||||
<small class="text-muted d-block mb-1">Ordered on @order.OrderDate.ToString("dd MMMM yyyy")</small>
|
<small class="text-muted d-block mb-1">Ordered on @order.OrderDate.ToString("dd MMMM yyyy")</small>
|
||||||
@@ -63,20 +64,26 @@
|
|||||||
<span>Shipped to: <span class="fw-semibold text-dark">@order.ShippingAddressName</span></span>
|
<span>Shipped to: <span class="fw-semibold text-dark">@order.ShippingAddressName</span></span>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-md-end d-flex flex-column align-items-md-end gap-2">
|
<div class="text-md-end d-flex flex-column align-items-md-end gap-2">
|
||||||
<div class="d-flex align-items-center gap-1.5 flex-wrap justify-content-md-end">
|
<div class="d-flex align-items-center gap-1.5 flex-wrap justify-content-md-end">
|
||||||
|
<span class="badge status-badge-base @GetOrderStatusClass(order.Status)">
|
||||||
|
Order: @order.Status
|
||||||
|
</span>
|
||||||
|
|
||||||
<span class="badge status-badge-base @GetPaymentStatusClass(order.PaymentStatus)">
|
<span class="badge status-badge-base @GetPaymentStatusClass(order.PaymentStatus)">
|
||||||
Pay: @order.PaymentStatus
|
Pay: @order.PaymentStatus
|
||||||
</span>
|
</span>
|
||||||
<span class="badge status-badge-base @GetStatusClass(order.Status)">
|
|
||||||
Logistics: @order.Status
|
<span class="badge status-badge-base @GetShippingStatusClass(order.ShippingStatus)">
|
||||||
|
Logistics: @order.ShippingStatus
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (order.PaymentStatus?.ToLower() == "paid")
|
@if (order.PaymentStatus?.ToLower() == "paid")
|
||||||
{
|
{
|
||||||
<button class="btn btn-outline-dark btn-premium-sm font-monospace text-uppercase d-inline-flex align-items-center gap-1.5 py-1 px-2.5"
|
<button class="btn btn-outline-dark btn-premium-sm font-monospace text-uppercase d-inline-flex align-items-center gap-1.5 py-1 px-2.5"
|
||||||
style="font-size: 0.7rem;"
|
style="font-size: 0.7rem;" disabled="@string.IsNullOrWhiteSpace(order.InvoiceUrl)"
|
||||||
@onclick="() => DownloadInvoice(order.OrderId)">
|
@onclick="() => DownloadInvoice(order.OrderId)">
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||||
<span>Invoice</span>
|
<span>Invoice</span>
|
||||||
@@ -85,12 +92,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="order-manifest-details d-flex align-items-center justify-content-between gap-4 py-1">
|
<div class="order-books-manifest manifest-grid-wrap mb-3">
|
||||||
<div class="item-meta">
|
@foreach (var book in order.PurchasedBooks)
|
||||||
<h6 class="text-dark fw-bold mb-0 small" style="line-height: 1.4;">@order.ProductTitle</h6>
|
{
|
||||||
|
<div class="manifest-grid-cell">
|
||||||
|
<div class="manifest-book-item border rounded p-2 d-flex align-items-center justify-content-between bg-light bg-opacity-20 h-100">
|
||||||
|
<div class="d-flex align-items-center gap-2.5 min-w-0">
|
||||||
|
<div class="book-thumbnail-container flex-shrink-0 border rounded bg-white shadow-xs">
|
||||||
|
<img src="@book.CoverImageUrl" alt="@book.Title" class="book-thumbnail-img" />
|
||||||
</div>
|
</div>
|
||||||
<div class="item-value text-end flex-shrink-0">
|
<div class="book-text-meta min-w-0">
|
||||||
<span class="font-monospace text-dark fw-bold d-block">R @order.Total.ToString("F2")</span>
|
<h6 class="text-dark fw-bold mb-0.5 small text-truncate" title="@book.Title">@book.Title</h6>
|
||||||
|
<span class="badge bg-white text-secondary border font-monospace extra-small py-0.5">Qty: @book.Quantity</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="book-row-pricing text-end font-monospace text-dark small ps-2 flex-shrink-0 fw-medium">
|
||||||
|
R @((book.PriceUnitPrice * book.Quantity).ToString("F2"))
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-summary-footer border-top pt-3 d-flex align-items-baseline justify-content-between">
|
||||||
|
<span class="text-muted small text-uppercase font-monospace">Order Total</span>
|
||||||
|
<div class="text-end">
|
||||||
|
<span class="font-monospace text-dark fw-bold h5 mb-0 d-block">R @order.Total.ToString("F2")</span>
|
||||||
<small class="text-muted extra-small font-monospace">VAT Inclusive</small>
|
<small class="text-muted extra-small font-monospace">VAT Inclusive</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +129,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="shipping" role="tabpanel">
|
<div class="tab-pane fade" id="shipping" role="tabpanel">
|
||||||
<div class="panel-card-wrapper mb-4">
|
<div class="tab-panel-body">
|
||||||
<div class="d-flex justify-content-between align-items-baseline mb-4">
|
<div class="d-flex justify-content-between align-items-baseline mb-4">
|
||||||
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-0">Saved Addresses</h5>
|
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-0">Saved Addresses</h5>
|
||||||
@if (!showAddForm && editingAddress == null)
|
@if (!showAddForm && editingAddress == null)
|
||||||
@@ -194,7 +221,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-pane fade" id="profile" role="tabpanel">
|
<div class="tab-pane fade" id="profile" role="tabpanel">
|
||||||
<div class="panel-card-wrapper mb-4">
|
<div class="tab-panel-body">
|
||||||
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Profile Settings</h5>
|
<h5 class="panel-section-title fw-bold text-dark font-monospace text-uppercase tracking-wider mb-4">Profile Settings</h5>
|
||||||
|
|
||||||
<div class="profile-hero-banner mb-4 d-flex align-items-center justify-content-between p-4 border rounded-3 bg-light bg-opacity-20 flex-wrap gap-3">
|
<div class="profile-hero-banner mb-4 d-flex align-items-center justify-content-between p-4 border rounded-3 bg-light bg-opacity-20 flex-wrap gap-3">
|
||||||
@@ -203,9 +230,7 @@
|
|||||||
<h5 class="fw-bold text-dark mb-1 h6">@User?.Identity?.Name</h5>
|
<h5 class="fw-bold text-dark mb-1 h6">@User?.Identity?.Name</h5>
|
||||||
<p class="text-muted small mb-0 font-monospace extra-small opacity-75">Secure Connection Authorized</p>
|
<p class="text-muted small mb-0 font-monospace extra-small opacity-75">Secure Connection Authorized</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge rounded-pill bg-success bg-opacity-10 text-success border border-success border-opacity-20 font-monospace px-3 py-1.5 small text-uppercase tracking-wide">
|
<span class="badge rounded-pill bg-success bg-opacity-10 text-success border border-success border-opacity-20 font-monospace px-3 py-1.5 small text-uppercase tracking-wide">Verified</span>
|
||||||
Verified
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card p-5 text-center bg-white border rounded-3 shadow-sm my-4">
|
<div class="card p-5 text-center bg-white border rounded-3 shadow-sm my-4">
|
||||||
@@ -219,7 +244,6 @@
|
|||||||
<p class="text-muted small mx-auto mb-4" style="max-width: 480px; line-height: 1.5;">
|
<p class="text-muted small mx-auto mb-4" style="max-width: 480px; line-height: 1.5;">
|
||||||
For your structural protection, password alterations, account recovery preferences, cross-tenant factors, and core credential manifests are handled through our global Identity Node security layer.
|
For your structural protection, password alterations, account recovery preferences, cross-tenant factors, and core credential manifests are handled through our global Identity Node security layer.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a href="https://sts.security.khongisa.co.za/Manage/Index?returnUrl=https://midrandbooks.co.za/account"
|
<a href="https://sts.security.khongisa.co.za/Manage/Index?returnUrl=https://midrandbooks.co.za/account"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
namespace MidrandBookshop.Components.Pages;
|
using LiteCharms.Features.Hasher;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Customers;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Orders;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Payments;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Products;
|
||||||
|
|
||||||
|
namespace MidrandBookshop.Components.Pages;
|
||||||
|
|
||||||
public partial class Account : ComponentBase
|
public partial class Account : ComponentBase
|
||||||
{
|
{
|
||||||
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
|
[Inject] private AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
|
||||||
|
[Inject] private CustomerService CustomerService { get; set; } = default!;
|
||||||
|
[Inject] private OrderService OrderService { get; set; } = default!;
|
||||||
|
[Inject] private PaymentService PaymentService { get; set; } = default!;
|
||||||
|
[Inject] private ProductService ProductService { get; set; } = default!;
|
||||||
|
[Inject] private HashService HashService { get; set; } = default!;
|
||||||
|
[Inject] private CancellationToken CancellationToken { get; set; } = default!;
|
||||||
|
[Inject] private IToastService ToasterService { get; set; } = default!;
|
||||||
|
|
||||||
private ClaimsPrincipal? User { get; set; }
|
private ClaimsPrincipal? User { get; set; }
|
||||||
|
private Customer? customer;
|
||||||
|
|
||||||
private bool showAddForm = false;
|
private bool showAddForm = false;
|
||||||
private AddressItem? editingAddress = null;
|
private AddressItem? editingAddress = null;
|
||||||
private string newAddressName = "";
|
private string newAddressName = "";
|
||||||
@@ -13,17 +29,7 @@ public partial class Account : ComponentBase
|
|||||||
private string newPostalCode = "";
|
private string newPostalCode = "";
|
||||||
private bool isBilling, isShipping;
|
private bool isBilling, isShipping;
|
||||||
|
|
||||||
private List<OrderItem> orderHistory = new()
|
private List<OrderItem> orderHistory = [];
|
||||||
{
|
|
||||||
// 1. Delivered + Paid (Green Mapping Rules)
|
|
||||||
new OrderItem { OrderId = "#MB-2026-8712", ProductId = "mastering-css-isolation", ProductTitle = "Mastering CSS Isolation in Modern .NET Web Applications Architecture", OrderDate = new DateTime(2026, 4, 14), ShippingAddressName = "Midrand Warehouse", Status = "Delivered", PaymentStatus = "Paid", Total = 890.00 },
|
|
||||||
|
|
||||||
// 2. Shipped + Paid (Amber Logistics + Green Payment Rules)
|
|
||||||
new OrderItem { OrderId = "#MB-2026-9481", ProductId = "introduction-to-blazor", ProductTitle = "Introduction to Blazor WebAssembly Framework Development", OrderDate = new DateTime(2026, 5, 20), ShippingAddressName = "Home Address", Status = "Shipped", PaymentStatus = "Paid", Total = 720.00 },
|
|
||||||
|
|
||||||
// 3. Unshipped + Abandoned/Unpaid (Muted Grey / Soft Red Rules — Hides Invoice Button)
|
|
||||||
new OrderItem { OrderId = "#MB-2026-1034", ProductId = "csharp-functional-paradigms", ProductTitle = "Advanced Functional Architecture & Monadic Paradigms in Modern C#", OrderDate = new DateTime(2026, 6, 11), ShippingAddressName = "Home Address", Status = "Unshipped", PaymentStatus = "Unpaid", Total = 650.00 }
|
|
||||||
};
|
|
||||||
|
|
||||||
private List<AddressItem> savedAddresses = new()
|
private List<AddressItem> savedAddresses = new()
|
||||||
{
|
{
|
||||||
@@ -35,64 +41,138 @@ public partial class Account : ComponentBase
|
|||||||
{
|
{
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||||
User = authState?.User;
|
User = authState?.User;
|
||||||
|
|
||||||
|
var customerFetch = await CustomerService.GetCustomerAsync(User?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)!.Value!, CancellationToken);
|
||||||
|
|
||||||
|
if (customerFetch.IsSuccess)
|
||||||
|
customer = customerFetch.Value;
|
||||||
|
|
||||||
|
await LoadOrdersAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadOrdersAsync()
|
||||||
|
{
|
||||||
|
if (customer is null)
|
||||||
|
{
|
||||||
|
ToasterService.ShowError("There was a problem loading your details, please contact the administrator");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ordersFetch = await OrderService.GetOrdersByCustomerAsync(customer.Id, CancellationToken);
|
||||||
|
|
||||||
|
if (ordersFetch.IsFailed)
|
||||||
|
{
|
||||||
|
ToasterService.ShowWarning("No orders were found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderHistory.Clear();
|
||||||
|
|
||||||
|
foreach (var order in ordersFetch.Value)
|
||||||
|
{
|
||||||
|
var paymentFetch = await PaymentService.GetOrderPaymentAsync(order.Id, CancellationToken);
|
||||||
|
|
||||||
|
var orderEntry = new OrderItem
|
||||||
|
{
|
||||||
|
OrderDate = order.CreatedAt,
|
||||||
|
OrderId = HashService.HashEncodeLongId(order.Id).Value,
|
||||||
|
Status = order.Status.ToString(),
|
||||||
|
PaymentStatus = paymentFetch.IsSuccess ? paymentFetch.Value.Status.ToString() : "NotPaid",
|
||||||
|
ShippingStatus = "Processing",
|
||||||
|
ShippingAddressName = "TBA",
|
||||||
|
Total = order.Total,
|
||||||
|
InvoiceUrl = order.InvoiceUrl!,
|
||||||
|
};
|
||||||
|
|
||||||
|
var orderItemsFetch = await OrderService.GetOrderItemsAsync(order.Id, CancellationToken);
|
||||||
|
if (orderItemsFetch.IsFailed) continue;
|
||||||
|
|
||||||
|
foreach (var item in orderItemsFetch.Value)
|
||||||
|
{
|
||||||
|
var productPriceFetch = await ProductService.GetProductPriceAsync(item.ProductPriceId, CancellationToken);
|
||||||
|
if (productPriceFetch.IsFailed) continue;
|
||||||
|
|
||||||
|
var productFetch = await ProductService.GetProductAsync(productPriceFetch.Value.ProductId, CancellationToken);
|
||||||
|
|
||||||
|
var itemEntry = new PurchasedBook
|
||||||
|
{
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
PriceUnitPrice = productPriceFetch.Value.Amount,
|
||||||
|
CoverImageUrl = productFetch.Value.ImageUrl!,
|
||||||
|
Title = productFetch.Value.Name!,
|
||||||
|
};
|
||||||
|
orderEntry.PurchasedBooks.Add(itemEntry);
|
||||||
|
}
|
||||||
|
orderHistory.Add(orderEntry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloadInvoice(string orderId)
|
private void DownloadInvoice(string orderId)
|
||||||
{
|
{
|
||||||
Navigation.NavigateTo($"/api/invoices/download/{orderId.Replace("#", "")}", forceLoad: true);
|
var order = orderHistory.FirstOrDefault(o => o.OrderId == orderId)!;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(order.InvoiceUrl))
|
||||||
|
ToasterService.ShowWarning("Your invoice is currently not availabe for viewing");
|
||||||
|
else
|
||||||
|
Navigation.NavigateTo(orderHistory.FirstOrDefault(o => o.OrderId == orderId)!.InvoiceUrl, forceLoad: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetStatusClass(string status) => status?.ToLower() switch
|
// Badge Style 1: Core Order Lifecycle Status Mapping
|
||||||
|
private string GetOrderStatusClass(string? status)
|
||||||
{
|
{
|
||||||
"delivered" => "status-delivered", // Green
|
return status?.ToLower() switch
|
||||||
"shipped" => "status-shipped", // Amber
|
{
|
||||||
_ => "status-processing" // Muted Architectural Dark Grey
|
"completed" => "order-completed",
|
||||||
|
"processing" => "order-processing",
|
||||||
|
"cancelled" => "order-cancelled",
|
||||||
|
_ => "order-hold"
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private string GetPaymentStatusClass(string paymentStatus) => paymentStatus?.ToLower() switch
|
// Badge Style 2: Financial Payment Status Mapping
|
||||||
|
private string GetPaymentStatusClass(string? status)
|
||||||
{
|
{
|
||||||
"paid" => "pay-paid", // Green
|
return status?.ToLower() switch
|
||||||
"refunded" => "pay-refunded", // Grey
|
{
|
||||||
_ => "pay-pending" // Red Alert Tone
|
"paid" => "pay-paid",
|
||||||
|
"refunded" => "pay-refunded",
|
||||||
|
_ => "pay-pending"
|
||||||
};
|
};
|
||||||
|
|
||||||
private void EditAddress(AddressItem addr)
|
|
||||||
{
|
|
||||||
editingAddress = addr;
|
|
||||||
showAddForm = false;
|
|
||||||
newAddressName = addr.Name;
|
|
||||||
newStreetAddress = addr.Street;
|
|
||||||
newCity = addr.City;
|
|
||||||
newPostalCode = addr.PostalCode;
|
|
||||||
isBilling = addr.IsBilling;
|
|
||||||
isShipping = addr.IsShipping;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CancelAddressActions()
|
// Badge Style 3: Logistics Shipment Status Mapping
|
||||||
|
private string GetShippingStatusClass(string? status)
|
||||||
{
|
{
|
||||||
showAddForm = false;
|
return status?.ToLower() switch
|
||||||
editingAddress = null;
|
{
|
||||||
ClearFormFields();
|
"delivered" => "status-delivered",
|
||||||
}
|
"shipped" => "status-shipped",
|
||||||
|
_ => "status-processing"
|
||||||
private void ClearFormFields()
|
};
|
||||||
{
|
|
||||||
newAddressName = "";
|
|
||||||
newStreetAddress = "";
|
|
||||||
newCity = "";
|
|
||||||
newPostalCode = "";
|
|
||||||
isBilling = false;
|
|
||||||
isShipping = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implemented to resolve UI registration click events
|
||||||
private void SaveAddress()
|
private void SaveAddress()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(newAddressName) || string.IsNullOrWhiteSpace(newStreetAddress)) return;
|
if (string.IsNullOrWhiteSpace(newAddressName) || string.IsNullOrWhiteSpace(newStreetAddress))
|
||||||
|
{
|
||||||
|
ToasterService.ShowWarning("Please fill in the required fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (editingAddress == null)
|
if (editingAddress != null)
|
||||||
|
{
|
||||||
|
editingAddress.Name = newAddressName;
|
||||||
|
editingAddress.Street = newStreetAddress;
|
||||||
|
editingAddress.City = newCity;
|
||||||
|
editingAddress.PostalCode = newPostalCode;
|
||||||
|
editingAddress.IsBilling = isBilling;
|
||||||
|
editingAddress.IsShipping = isShipping;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1;
|
var nextId = savedAddresses.Any() ? savedAddresses.Max(a => a.Id) + 1 : 1;
|
||||||
var newAddr = new AddressItem
|
savedAddresses.Add(new AddressItem
|
||||||
{
|
{
|
||||||
Id = nextId,
|
Id = nextId,
|
||||||
Name = newAddressName,
|
Name = newAddressName,
|
||||||
@@ -102,26 +182,39 @@ public partial class Account : ComponentBase
|
|||||||
IsBilling = isBilling,
|
IsBilling = isBilling,
|
||||||
IsShipping = isShipping,
|
IsShipping = isShipping,
|
||||||
IsPrimary = !savedAddresses.Any()
|
IsPrimary = !savedAddresses.Any()
|
||||||
};
|
});
|
||||||
savedAddresses.Add(newAddr);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var target = savedAddresses.FirstOrDefault(a => a.Id == editingAddress.Id);
|
|
||||||
if (target != null)
|
|
||||||
{
|
|
||||||
target.Name = newAddressName;
|
|
||||||
target.Street = newStreetAddress;
|
|
||||||
target.City = newCity;
|
|
||||||
target.PostalCode = newPostalCode;
|
|
||||||
target.IsBilling = isBilling;
|
|
||||||
target.IsShipping = isShipping;
|
|
||||||
}
|
|
||||||
editingAddress = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CancelAddressActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelAddressActions()
|
||||||
|
{
|
||||||
|
showAddForm = false;
|
||||||
|
editingAddress = null;
|
||||||
|
ResetFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetFormFields()
|
||||||
|
{
|
||||||
|
newAddressName = "";
|
||||||
|
newStreetAddress = "";
|
||||||
|
newCity = "";
|
||||||
|
newPostalCode = "";
|
||||||
|
isBilling = false;
|
||||||
|
isShipping = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditAddress(AddressItem addr)
|
||||||
|
{
|
||||||
|
editingAddress = addr;
|
||||||
|
newAddressName = addr.Name;
|
||||||
|
newStreetAddress = addr.Street;
|
||||||
|
newCity = addr.City;
|
||||||
|
newPostalCode = addr.PostalCode;
|
||||||
|
isBilling = addr.IsBilling;
|
||||||
|
isShipping = addr.IsShipping;
|
||||||
showAddForm = false;
|
showAddForm = false;
|
||||||
ClearFormFields();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DeleteAddress(AddressItem addr)
|
private void DeleteAddress(AddressItem addr)
|
||||||
@@ -161,12 +254,21 @@ public partial class Account : ComponentBase
|
|||||||
public class OrderItem
|
public class OrderItem
|
||||||
{
|
{
|
||||||
public string OrderId { get; set; } = "";
|
public string OrderId { get; set; } = "";
|
||||||
public string ProductId { get; set; } = "";
|
|
||||||
public string ProductTitle { get; set; } = "";
|
|
||||||
public DateTime OrderDate { get; set; }
|
public DateTime OrderDate { get; set; }
|
||||||
public string ShippingAddressName { get; set; } = "";
|
public string ShippingAddressName { get; set; } = "";
|
||||||
public string Status { get; set; } = "";
|
public string Status { get; set; } = "";
|
||||||
public string PaymentStatus { get; set; } = "Pending";
|
public string PaymentStatus { get; set; } = "";
|
||||||
public double Total { get; set; }
|
public string ShippingStatus { get; set; } = "";
|
||||||
|
public string InvoiceUrl { get; set; } = "";
|
||||||
|
public decimal Total { get; set; }
|
||||||
|
public List<PurchasedBook> PurchasedBooks { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PurchasedBook
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string CoverImageUrl { get; set; } = "";
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
public decimal PriceUnitPrice { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,13 +3,25 @@
|
|||||||
========================================================================== */
|
========================================================================== */
|
||||||
|
|
||||||
.account-page-container {
|
.account-page-container {
|
||||||
max-width: 1140px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-panel-body {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.tab-panel-body {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account-main-title {
|
.account-main-title {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: -0.03em;
|
||||||
@@ -39,102 +51,99 @@
|
|||||||
.account-nav-stack .nav-link.active {
|
.account-nav-stack .nav-link.active {
|
||||||
background-color: #111111 !important;
|
background-color: #111111 !important;
|
||||||
color: #FFFFFF !important;
|
color: #FFFFFF !important;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-nav-stack .nav-link:hover:not(.active):not(.nav-logout) {
|
.account-nav-stack .nav-link:hover:not(.active) {
|
||||||
color: #111111;
|
|
||||||
background-color: rgba(0, 0, 0, 0.04) !important;
|
background-color: rgba(0, 0, 0, 0.04) !important;
|
||||||
transform: translateX(2px);
|
color: #111111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-nav-stack .nav-logout:hover {
|
.nav-logout:hover {
|
||||||
background-color: #FFF5F5 !important;
|
background-color: rgba(220, 53, 69, 0.08) !important;
|
||||||
color: #DC3545 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Main Tabbed Layout Container Content Structures --- */
|
/* --- Balanced Status Mapping Badges --- */
|
||||||
.panel-card-wrapper {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 2.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-section-title {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Order History Structured Panel Cards --- */
|
|
||||||
.premium-order-card {
|
|
||||||
transition: transform 0.2s ease, border-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.premium-order-card:hover {
|
|
||||||
border-color: rgba(0, 0, 0, 0.15) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-b-dashed {
|
|
||||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Minimalist Curated Logistics and Payment Badges Matrix */
|
|
||||||
.status-badge-base {
|
.status-badge-base {
|
||||||
font-family: var(--bs-font-monospace);
|
font-family: var(--bs-font-monospace);
|
||||||
font-size: 0.65rem !important;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05em;
|
font-size: 0.68rem;
|
||||||
padding: 0.3rem 0.6rem !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fulfillment Matrix Colors */
|
/* 1. Core Order Lifecycle Badge Styles */
|
||||||
|
.order-completed {
|
||||||
|
background-color: #111111 !important;
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
border: 1px solid #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-processing {
|
||||||
|
background-color: rgba(0, 0, 0, 0.03) !important;
|
||||||
|
color: #111111 !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-hold {
|
||||||
|
background-color: rgba(108, 117, 125, 0.05) !important;
|
||||||
|
color: #495057 !important;
|
||||||
|
border: 1px solid rgba(108, 117, 125, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-cancelled {
|
||||||
|
background-color: rgba(220, 53, 69, 0.04) !important;
|
||||||
|
color: #842029 !important;
|
||||||
|
border: 1px solid rgba(220, 53, 69, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Logistics Shipment Status Badge Styles */
|
||||||
.status-delivered {
|
.status-delivered {
|
||||||
background-color: #E2F0D9 !important; /* Soft Green Match */
|
background-color: rgba(25, 135, 84, 0.06) !important;
|
||||||
color: #385723 !important;
|
color: #198754 !important;
|
||||||
border: 1px solid rgba(56, 87, 35, 0.15);
|
border: 1px solid rgba(25, 135, 84, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-shipped {
|
.status-shipped {
|
||||||
background-color: #FFF3CD !important; /* Warm Gold Amber */
|
background-color: rgba(13, 110, 253, 0.06) !important;
|
||||||
color: #856404 !important;
|
color: #0d6efd !important;
|
||||||
border: 1px solid rgba(133, 100, 4, 0.12);
|
border: 1px solid rgba(13, 110, 253, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-processing {
|
.status-processing {
|
||||||
background-color: #F2F2F2 !important; /* Architectural Neutral Muted Grey */
|
background-color: rgba(255, 193, 7, 0.08) !important;
|
||||||
color: #595959 !important;
|
color: #b58100 !important;
|
||||||
border: 1px dashed rgba(0, 0, 0, 0.12);
|
border: 1px solid rgba(255, 193, 7, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Financial Matrix Colors */
|
/* 3. Financial Payment Status Badge Styles */
|
||||||
.pay-paid {
|
.pay-paid {
|
||||||
background-color: #E2F0D9 !important; /* Soft Green Match */
|
background-color: rgba(25, 135, 84, 0.06) !important;
|
||||||
color: #385723 !important;
|
color: #198754 !important;
|
||||||
border: 1px solid rgba(56, 87, 35, 0.15);
|
border: 1px solid rgba(25, 135, 84, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-pending {
|
.pay-pending {
|
||||||
background-color: #F8D7DA !important; /* soft red alert tint for unpaid/abandoned items */
|
background-color: rgba(220, 53, 69, 0.06) !important;
|
||||||
color: #721C24 !important;
|
color: #dc3545 !important;
|
||||||
border: 1px solid rgba(114, 28, 36, 0.15);
|
border: 1px solid rgba(220, 53, 69, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pay-refunded {
|
.pay-refunded {
|
||||||
background-color: #E2E3E5 !important;
|
background-color: rgba(108, 117, 125, 0.08) !important;
|
||||||
color: #383D41 !important;
|
color: #6c757d !important;
|
||||||
border: 1px solid rgba(56, 61, 65, 0.15);
|
border: 1px solid rgba(108, 117, 125, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Curated Shipping Identity Panels --- */
|
/* --- Saved Addresses Section Layout --- */
|
||||||
.address-curated-card {
|
.address-curated-card {
|
||||||
transition: all 0.23s cubic-bezier(0.16, 1, 0.3, 1);
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-curated-card:hover {
|
.address-curated-card:hover {
|
||||||
border-color: #111111 !important;
|
border-color: #111111 !important;
|
||||||
transform: translateY(-2px);
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.03) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-top-dashed {
|
.border-top-dashed {
|
||||||
@@ -142,22 +151,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-action-trigger {
|
.btn-action-trigger {
|
||||||
transition: color 0.15s ease;
|
padding: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
opacity: 0.75;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-action-trigger:hover:not(.text-danger) {
|
.btn-action-trigger:hover {
|
||||||
color: #111111 !important;
|
opacity: 1;
|
||||||
text-decoration: underline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-action-trigger:hover.text-danger {
|
/* --- Interactive Address Form Fields --- */
|
||||||
color: #A71D2A !important;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Interactive Form Element Overlays --- */
|
|
||||||
.premium-plaintext-field {
|
.premium-plaintext-field {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
background-color: #FAFAFA;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.65rem 0.85rem;
|
padding: 0.65rem 0.85rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
@@ -196,40 +204,69 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: #666666;
|
color: #666666;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-clean-cancel:hover {
|
.btn-clean-cancel:hover {
|
||||||
color: #111111;
|
color: #111111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-y {
|
/* --- Profile Settings Identity Pillar Elements --- */
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Identity Node Presentation Wrappers --- */
|
|
||||||
.profile-hero-banner {
|
.profile-hero-banner {
|
||||||
|
background-color: #FAFAFA;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in {
|
/* --- CSS Grid Book Item Layout --- */
|
||||||
animation: layoutFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
.manifest-grid-wrap {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes layoutFadeIn {
|
@media (min-width: 768px) {
|
||||||
from {
|
.manifest-grid-wrap {
|
||||||
opacity: 0;
|
grid-template-columns: repeat(2, 1fr);
|
||||||
transform: translateY(6px);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
.manifest-grid-cell {
|
||||||
opacity: 1;
|
min-width: 0;
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manifest-book-item {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-book-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.015) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-thumbnail-container {
|
||||||
|
width: 42px;
|
||||||
|
height: 56px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-thumbnail-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-xs {
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-w-0 {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary-footer {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user