Enhance dashboard UI and add order timeline
All checks were successful
Build Frontend / build (push) Successful in 1m12s
All checks were successful
Build Frontend / build (push) Successful in 1m12s
Refactored dashboard pages for improved layout and visual consistency using Card components, motion animations, and updated color schemes. Added an OrderTimeline component to the order details page to visualize order lifecycle. Improved customer management page with better sorting, searching, and a detailed customer dialog. Updated storefront settings page with a modernized layout and clearer sectioning.
This commit is contained in:
@@ -39,6 +39,8 @@ import {
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import Layout from "@/components/layout/layout";
|
||||
import { cacheUtils } from '@/lib/api-client';
|
||||
import OrderTimeline from "@/components/orders/order-timeline";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
interface Order {
|
||||
orderId: string;
|
||||
@@ -170,7 +172,7 @@ export default function OrderDetailsPage() {
|
||||
authToken: string
|
||||
): Promise<Record<string, string>> => {
|
||||
const productNamesMap: Record<string, string> = {};
|
||||
|
||||
|
||||
// Process each product ID independently
|
||||
const fetchPromises = productIds.map(async (id) => {
|
||||
try {
|
||||
@@ -184,10 +186,10 @@ export default function OrderDetailsPage() {
|
||||
productNamesMap[id] = "Unknown Product (Deleted)";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Wait for all fetch operations to complete (successful or failed)
|
||||
await Promise.all(fetchPromises);
|
||||
|
||||
|
||||
return productNamesMap;
|
||||
};
|
||||
|
||||
@@ -195,38 +197,38 @@ export default function OrderDetailsPage() {
|
||||
try {
|
||||
// Add a loading state to give feedback
|
||||
const loadingToast = toast.loading("Marking order as paid...");
|
||||
|
||||
|
||||
// Log the request for debugging
|
||||
console.log(`Sending request to /orders/${orderId}/status with clientFetch`);
|
||||
console.log("Request payload:", { status: "paid" });
|
||||
|
||||
|
||||
// Use clientFetch which handles API URL and auth token automatically
|
||||
const response = await clientFetch(`/orders/${orderId}/status`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ status: "paid" }),
|
||||
});
|
||||
|
||||
|
||||
// Log the response
|
||||
console.log("API response:", response);
|
||||
|
||||
toast.dismiss(loadingToast);
|
||||
|
||||
|
||||
if (response && response.message === "Order status updated successfully") {
|
||||
// Update both states
|
||||
setIsPaid(true);
|
||||
setOrder((prevOrder) => (prevOrder ? {
|
||||
...prevOrder,
|
||||
setOrder((prevOrder) => (prevOrder ? {
|
||||
...prevOrder,
|
||||
status: "paid",
|
||||
// Clear underpayment flags when marking as paid
|
||||
underpaid: false,
|
||||
underpaymentAmount: 0
|
||||
} : null));
|
||||
|
||||
|
||||
// Invalidate order cache to ensure fresh data everywhere
|
||||
cacheUtils.invalidateOrderData(orderId as string);
|
||||
|
||||
|
||||
toast.success("Order marked as paid successfully");
|
||||
|
||||
|
||||
// Refresh order data to get latest status
|
||||
setTimeout(() => {
|
||||
setRefreshTrigger(prev => prev + 1);
|
||||
@@ -240,14 +242,14 @@ export default function OrderDetailsPage() {
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to mark order as paid:", error);
|
||||
|
||||
|
||||
// More detailed error handling
|
||||
let errorMessage = "Failed to mark order as paid";
|
||||
|
||||
|
||||
if (error.message) {
|
||||
errorMessage += `: ${error.message}`;
|
||||
}
|
||||
|
||||
|
||||
if (error.response) {
|
||||
try {
|
||||
const errorData = await error.response.json();
|
||||
@@ -259,7 +261,7 @@ export default function OrderDetailsPage() {
|
||||
console.error("Could not parse error response:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
@@ -317,7 +319,7 @@ export default function OrderDetailsPage() {
|
||||
...prevOrder,
|
||||
trackingNumber: trackingNumber
|
||||
} : null);
|
||||
|
||||
|
||||
toast.success("Tracking number added successfully!");
|
||||
} catch (err: any) {
|
||||
console.error("Failed to add tracking number:", err);
|
||||
@@ -330,7 +332,7 @@ export default function OrderDetailsPage() {
|
||||
const handleMarkAsAcknowledged = async () => {
|
||||
try {
|
||||
setIsAcknowledging(true);
|
||||
|
||||
|
||||
// Use clientFetch which handles API URL and auth token automatically
|
||||
const response = await clientFetch(`/orders/${orderId}/status`, {
|
||||
method: "PUT",
|
||||
@@ -382,7 +384,7 @@ export default function OrderDetailsPage() {
|
||||
const handleCancelOrder = async () => {
|
||||
try {
|
||||
setIsCancelling(true);
|
||||
|
||||
|
||||
// Use clientFetch which handles API URL and auth token automatically
|
||||
const response = await clientFetch(`/orders/${orderId}/status`, {
|
||||
method: "PUT",
|
||||
@@ -429,10 +431,10 @@ export default function OrderDetailsPage() {
|
||||
const productIds = data.order.products.map((product) => product.productId);
|
||||
const productNamesMap = await fetchProductNames(productIds, authToken);
|
||||
setProductNames(productNamesMap);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
setProductNames(prev => {
|
||||
const newMap = {...prev};
|
||||
const newMap = { ...prev };
|
||||
productIds.forEach(id => {
|
||||
if (!newMap[id] || newMap[id] === "Loading...") {
|
||||
newMap[id] = "Unknown Product (Deleted)";
|
||||
@@ -440,7 +442,7 @@ export default function OrderDetailsPage() {
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
}, 3000);
|
||||
}, 3000);
|
||||
|
||||
if (data.order.status === "paid") {
|
||||
setIsPaid(true);
|
||||
@@ -460,7 +462,7 @@ export default function OrderDetailsPage() {
|
||||
const fetchAdjacentOrders = async () => {
|
||||
try {
|
||||
const authToken = document.cookie.split("Authorization=")[1];
|
||||
|
||||
|
||||
if (!order?.orderId) return;
|
||||
|
||||
// Get the current numerical orderId
|
||||
@@ -470,7 +472,7 @@ export default function OrderDetailsPage() {
|
||||
// Use the new optimized backend endpoint to get adjacent orders
|
||||
const adjacentOrdersUrl = `${process.env.NEXT_PUBLIC_API_URL}/orders/adjacent/${currentOrderId}`;
|
||||
console.log('Fetching adjacent orders:', adjacentOrdersUrl);
|
||||
|
||||
|
||||
const adjacentOrdersRes = await fetchData(
|
||||
adjacentOrdersUrl,
|
||||
{
|
||||
@@ -488,17 +490,17 @@ export default function OrderDetailsPage() {
|
||||
|
||||
// Set the next and previous order IDs
|
||||
const { newer, older } = adjacentOrdersRes;
|
||||
|
||||
|
||||
// Set IDs for navigation
|
||||
setPrevOrderId(newer?._id || null);
|
||||
setNextOrderId(older?._id || null);
|
||||
|
||||
|
||||
if (newer) {
|
||||
console.log(`Newer order: ${newer.orderId} (ID: ${newer._id})`);
|
||||
} else {
|
||||
console.log('No newer order found');
|
||||
}
|
||||
|
||||
|
||||
if (older) {
|
||||
console.log(`Older order: ${older.orderId} (ID: ${older._id})`);
|
||||
} else {
|
||||
@@ -544,7 +546,7 @@ export default function OrderDetailsPage() {
|
||||
...prevOrder,
|
||||
trackingNumber: trackingNumber
|
||||
} : null);
|
||||
|
||||
|
||||
toast.success("Tracking number updated successfully!");
|
||||
setTrackingNumber(""); // Clear the input
|
||||
} catch (err: any) {
|
||||
@@ -569,11 +571,11 @@ export default function OrderDetailsPage() {
|
||||
|
||||
try {
|
||||
const lines = [];
|
||||
|
||||
|
||||
// Order number
|
||||
lines.push(`Order Number: ${order.orderId}`);
|
||||
lines.push('');
|
||||
|
||||
|
||||
// Order details
|
||||
lines.push('Order Details:');
|
||||
if (order.products && order.products.length > 0) {
|
||||
@@ -582,30 +584,30 @@ export default function OrderDetailsPage() {
|
||||
lines.push(` - ${productName} (Qty: ${product.quantity} @ £${product.pricePerUnit.toFixed(2)} = £${product.totalItemPrice.toFixed(2)})`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Shipping
|
||||
if (order.shippingMethod) {
|
||||
lines.push(` - Shipping: ${order.shippingMethod.type} (£${order.shippingMethod.price.toFixed(2)})`);
|
||||
}
|
||||
|
||||
|
||||
// Discount
|
||||
if (order.discountAmount && order.discountAmount > 0) {
|
||||
lines.push(` - Discount: -£${order.discountAmount.toFixed(2)}${order.promotionCode ? ` (Promo: ${order.promotionCode})` : ''}`);
|
||||
}
|
||||
|
||||
|
||||
// Subtotal if different from total
|
||||
if (order.subtotalBeforeDiscount && order.subtotalBeforeDiscount !== order.totalPrice) {
|
||||
lines.push(` - Subtotal: £${order.subtotalBeforeDiscount.toFixed(2)}`);
|
||||
}
|
||||
|
||||
|
||||
// Total
|
||||
lines.push(` - Total: £${order.totalPrice.toFixed(2)}`);
|
||||
lines.push('');
|
||||
|
||||
|
||||
// Address
|
||||
lines.push('Address:');
|
||||
lines.push(order.pgpAddress || 'N/A');
|
||||
|
||||
|
||||
const textToCopy = lines.join('\n');
|
||||
await navigator.clipboard.writeText(textToCopy);
|
||||
toast.success("Order data copied to clipboard!");
|
||||
@@ -618,28 +620,28 @@ export default function OrderDetailsPage() {
|
||||
// Helper function to check if order is underpaid
|
||||
const isOrderUnderpaid = (order: Order | null) => {
|
||||
// More robust check - only show underpaid if status is NOT paid and underpayment exists
|
||||
return order?.underpaid === true &&
|
||||
order?.underpaymentAmount &&
|
||||
order.underpaymentAmount > 0 &&
|
||||
order.status !== "paid" &&
|
||||
order.status !== "completed" &&
|
||||
order.status !== "shipped";
|
||||
return order?.underpaid === true &&
|
||||
order?.underpaymentAmount &&
|
||||
order.underpaymentAmount > 0 &&
|
||||
order.status !== "paid" &&
|
||||
order.status !== "completed" &&
|
||||
order.status !== "shipped";
|
||||
};
|
||||
|
||||
// Helper function to get underpaid information
|
||||
const getUnderpaidInfo = (order: Order | null) => {
|
||||
if (!isOrderUnderpaid(order)) return null;
|
||||
|
||||
|
||||
const received = order?.lastBalanceReceived || 0;
|
||||
const required = order?.cryptoTotal || 0;
|
||||
const missing = order?.underpaymentAmount || 0;
|
||||
|
||||
|
||||
// Calculate LTC to GBP exchange rate from order data
|
||||
const ltcToGbpRate = required > 0 ? (order?.totalPrice || 0) / required : 0;
|
||||
const receivedGbp = received * ltcToGbpRate;
|
||||
const requiredGbp = order?.totalPrice || 0;
|
||||
const missingGbp = missing * ltcToGbpRate;
|
||||
|
||||
|
||||
return {
|
||||
received,
|
||||
required,
|
||||
@@ -772,7 +774,7 @@ export default function OrderDetailsPage() {
|
||||
<p className="font-semibold">{underpaidInfo.percentage}% paid</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{order?.paymentAddress && (
|
||||
<div className="pt-3 border-t border-red-200 dark:border-red-800">
|
||||
<p className="text-sm text-muted-foreground mb-2">Payment Address:</p>
|
||||
@@ -791,10 +793,26 @@ export default function OrderDetailsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* Order Timeline */}
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
|
||||
<CardHeader className="pb-0">
|
||||
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Order Lifecycle</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OrderTimeline
|
||||
status={order?.status || ''}
|
||||
orderDate={order?.orderDate || ''}
|
||||
paidAt={order?.paidAt}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
className="grid grid-cols-3 gap-6"
|
||||
>
|
||||
{/* Left Column - Order Details */}
|
||||
<div className="col-span-2 space-y-6">
|
||||
{/* Products Card */}
|
||||
@@ -929,7 +947,7 @@ export default function OrderDetailsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-zinc-400">Customer Since</span>
|
||||
<span className="font-medium">
|
||||
{customerInsights.firstOrder ?
|
||||
{customerInsights.firstOrder ?
|
||||
new Date(customerInsights.firstOrder).toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
@@ -1092,34 +1110,34 @@ export default function OrderDetailsPage() {
|
||||
)}
|
||||
|
||||
{/* Cancel Order Button */}
|
||||
{order?.status !== "cancelled" &&
|
||||
order?.status !== "completed" &&
|
||||
order?.status !== "shipped" && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-full">
|
||||
Cancel Order
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Cancel Order</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to cancel this order? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleCancelOrder}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
Confirm Cancel
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
{order?.status !== "cancelled" &&
|
||||
order?.status !== "completed" &&
|
||||
order?.status !== "shipped" && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-full">
|
||||
Cancel Order
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Cancel Order</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to cancel this order? This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleCancelOrder}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
Confirm Cancel
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
|
||||
{/* No Actions Available Message */}
|
||||
{(order?.status === "completed" || order?.status === "cancelled") && (
|
||||
@@ -1168,11 +1186,10 @@ export default function OrderDetailsPage() {
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-4 h-4 ${
|
||||
i < (order?.review?.stars || 0)
|
||||
className={`w-4 h-4 ${i < (order?.review?.stars || 0)
|
||||
? "text-yellow-400"
|
||||
: "text-zinc-600"
|
||||
}`}
|
||||
}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
@@ -1198,10 +1215,10 @@ export default function OrderDetailsPage() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shipping Dialog removed; use inline tracking input above */}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Shipping Dialog removed; use inline tracking input above */}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user