Introduces an exportOrdersToCSV function in lib/api-client.ts to allow exporting orders by status as a CSV file. Updates various UI components to use the '•' (bullet) symbol instead of '·' (middle dot) and replaces some emoji/unicode characters for improved consistency and compatibility. Also normalizes the 'use client' directive to include a BOM in many files.
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
"use client";
|
||
import { useEffect, useState } from "react";
|
||
import { fetchClient } from "@/lib/api-client";
|
||
|
||
interface OrderItem {
|
||
name: string;
|
||
quantity: number;
|
||
}
|
||
|
||
interface Order {
|
||
orderId: number | string;
|
||
userId: number;
|
||
total: number;
|
||
createdAt: string;
|
||
status?: string;
|
||
items?: OrderItem[];
|
||
}
|
||
|
||
export default function RecentOrdersCard() {
|
||
const [orders, setOrders] = useState<Order[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
function statusBadgeClass(status: string) {
|
||
switch (status) {
|
||
case 'unpaid':
|
||
case 'confirming':
|
||
return 'bg-amber-500/15 text-amber-400';
|
||
case 'paid':
|
||
case 'acknowledged':
|
||
return 'bg-sky-500/15 text-sky-400';
|
||
case 'shipped':
|
||
return 'bg-purple-500/15 text-purple-400';
|
||
case 'completed':
|
||
return 'bg-emerald-500/15 text-emerald-400';
|
||
case 'cancelled':
|
||
return 'bg-rose-500/15 text-rose-400';
|
||
default:
|
||
return 'bg-muted text-foreground/70';
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
let mounted = true;
|
||
(async () => {
|
||
try {
|
||
const data = await fetchClient<Order[]>("/admin/recent-orders");
|
||
if (mounted) setOrders(data);
|
||
} catch (e: any) {
|
||
if (mounted) setError(e?.message || "Failed to load orders");
|
||
} finally {
|
||
if (mounted) setLoading(false);
|
||
}
|
||
})();
|
||
return () => { mounted = false; };
|
||
}, []);
|
||
|
||
return (
|
||
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]">
|
||
<h2 className="font-medium">Recent Orders</h2>
|
||
<p className="text-sm text-muted-foreground mt-1">Last 10 orders across stores</p>
|
||
{loading ? (
|
||
<p className="text-sm text-muted-foreground mt-3">Loading...</p>
|
||
) : error ? (
|
||
<p className="text-sm text-muted-foreground mt-3">{error}</p>
|
||
) : orders.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground mt-3">No recent orders</p>
|
||
) : (
|
||
<div className="mt-3 space-y-3">
|
||
{orders.slice(0, 10).map((o) => (
|
||
<div key={String(o.orderId)} className="rounded border border-border/50 p-3">
|
||
<div className="flex items-center justify-between text-sm">
|
||
<div className="font-medium">Order #{o.orderId}</div>
|
||
<div className="flex items-center gap-2">
|
||
{o.status && (
|
||
<span className={`text-xs px-2 py-0.5 rounded ${statusBadgeClass(o.status)}`}>
|
||
{o.status}
|
||
</span>
|
||
)}
|
||
<div className="text-muted-foreground">{new Date(o.createdAt).toLocaleString()}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1 text-xs text-muted-foreground">
|
||
User: {o.userId} • Total: £{Number(o.total).toFixed(2)}
|
||
</div>
|
||
{o.items && o.items.length > 0 && (
|
||
<ul className="mt-2 text-xs list-disc pl-4 text-muted-foreground">
|
||
{o.items.map((it, idx) => (
|
||
<li key={idx}>{it.name} × {it.quantity}</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|