- Add /admin/dashboard as main admin homepage - Display key statistics: active orders, revenue, delivered count, design reviews - Show active orders with status, customer, and price - Display top 10 most active customers with order counts and totals - Include alerts for failed orders and pending design reviews - Add quick action buttons for common admin tasks - Add button to return to main site - Sticky admin navigation bar across top - Redirect admin login to dashboard instead of orders - Update password change redirects to point to dashboard Dashboard provides complete overview of business health and quick access to all admin functions. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
236 lines
9.9 KiB
TypeScript
236 lines
9.9 KiB
TypeScript
import Link from 'next/link';
|
|
import { prisma } from '@/lib/prisma';
|
|
import { isAdminAuthenticated } from '@/lib/adminAuth';
|
|
import AdminNav from '@/components/AdminNav';
|
|
|
|
export default async function AdminDashboard() {
|
|
const isAdmin = await isAdminAuthenticated();
|
|
if (!isAdmin) {
|
|
return <div className="px-6 py-12 text-center">Unauthorized</div>;
|
|
}
|
|
|
|
// Fetch statistics
|
|
const orders = await prisma.order.findMany({
|
|
include: { items: true, customer: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
|
|
const activeOrders = orders.filter((o) => ['PENDING', 'PAID'].includes(o.status));
|
|
const totalRevenue = orders
|
|
.filter((o) => o.status === 'PAID')
|
|
.reduce((sum, o) => sum + o.total, 0);
|
|
const deliveredCount = orders.filter((o) => o.status === 'DELIVERED').length;
|
|
|
|
// Get top 10 active customers (by order count)
|
|
const customers = await prisma.customer.findMany({
|
|
include: { orders: true },
|
|
});
|
|
const topCustomers = customers
|
|
.map((c) => ({
|
|
id: c.id,
|
|
name: c.name || c.email,
|
|
email: c.email,
|
|
orderCount: c.orders.length,
|
|
totalSpent: c.orders.reduce((sum, o) => sum + o.total, 0),
|
|
}))
|
|
.sort((a, b) => b.orderCount - a.orderCount)
|
|
.slice(0, 10);
|
|
|
|
// Get pending design approvals
|
|
const designApprovals = await prisma.designApproval.findMany({
|
|
where: { status: 'IN_REVIEW' },
|
|
include: { product: true },
|
|
});
|
|
|
|
// Get failed orders
|
|
const failedOrders = orders.filter((o) => o.status === 'FAILED');
|
|
|
|
// Recent orders (last 5)
|
|
const recentOrders = orders.slice(0, 5);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-surface-1">
|
|
{/* Header */}
|
|
<div className="border-b border-line bg-surface">
|
|
<div className="mx-auto max-w-6xl px-6 py-6">
|
|
<div className="flex items-baseline justify-between">
|
|
<h1 className="font-display text-3xl">Admin Dashboard</h1>
|
|
<Link href="/" className="text-sm text-clay hover:text-clay-dark font-medium">
|
|
← Back to site
|
|
</Link>
|
|
</div>
|
|
<p className="mt-2 text-sm text-muted">
|
|
Overview of orders, customers, and key metrics
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Navigation */}
|
|
<div className="border-b border-line bg-surface sticky top-0 z-10">
|
|
<div className="mx-auto max-w-6xl px-6">
|
|
<AdminNav />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mx-auto max-w-6xl px-6 py-12">
|
|
{/* Key Statistics */}
|
|
<div className="grid gap-4 sm:grid-cols-4 mb-12">
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<p className="tag-label text-muted">Active Orders</p>
|
|
<p className="mt-3 font-display text-3xl">{activeOrders.length}</p>
|
|
<p className="mt-1 text-xs text-muted">Pending or paid</p>
|
|
</div>
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<p className="tag-label text-muted">Revenue (Paid)</p>
|
|
<p className="mt-3 font-display text-3xl">£{(totalRevenue / 100).toFixed(2)}</p>
|
|
<p className="mt-1 text-xs text-muted">{orders.filter((o) => o.status === 'PAID').length} orders</p>
|
|
</div>
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<p className="tag-label text-muted">Delivered</p>
|
|
<p className="mt-3 font-display text-3xl">{deliveredCount}</p>
|
|
<p className="mt-1 text-xs text-muted">Completed orders</p>
|
|
</div>
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<p className="tag-label text-muted">Design Reviews</p>
|
|
<p className="mt-3 font-display text-3xl">{designApprovals.length}</p>
|
|
<p className="mt-1 text-xs text-muted">Awaiting approval</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content Grid */}
|
|
<div className="grid gap-8 lg:grid-cols-3">
|
|
{/* Active Orders */}
|
|
<div className="lg:col-span-2">
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<div className="flex items-baseline justify-between mb-4">
|
|
<h2 className="font-display text-2xl">Active Orders</h2>
|
|
<Link href="/admin/orders" className="text-sm text-clay hover:text-clay-dark">
|
|
View all →
|
|
</Link>
|
|
</div>
|
|
|
|
{activeOrders.length === 0 ? (
|
|
<p className="text-sm text-muted py-8">No active orders</p>
|
|
) : (
|
|
<div className="divide-y divide-line">
|
|
{activeOrders.slice(0, 8).map((order) => (
|
|
<Link
|
|
key={order.id}
|
|
href={`/admin/orders/${order.id}`}
|
|
className="flex items-center justify-between py-3 hover:bg-surface-1 px-2 -mx-2 rounded"
|
|
>
|
|
<div className="flex-1">
|
|
<p className="font-medium text-sm">
|
|
Order #{order.id.slice(0, 8).toUpperCase()}
|
|
</p>
|
|
<p className="text-xs text-muted">
|
|
{order.email || order.guestName || 'Guest'} · {order.items.length} item
|
|
{order.items.length !== 1 ? 's' : ''}
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="font-mono text-sm">£{(order.total / 100).toFixed(2)}</p>
|
|
<span
|
|
className={`tag-label text-xs px-2 py-1 rounded-full inline-block mt-1 ${
|
|
order.status === 'PENDING'
|
|
? 'bg-splash-orange/10 text-splash-orange'
|
|
: 'bg-splash-blue/10 text-splash-blue'
|
|
}`}
|
|
>
|
|
{order.status === 'PENDING' ? 'Awaiting payment' : 'Paid'}
|
|
</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Alerts */}
|
|
{failedOrders.length > 0 && (
|
|
<div className="border border-splash-pink/40 bg-splash-pink/5 p-4 rounded mt-8">
|
|
<p className="font-medium text-splash-pink mb-2">⚠ {failedOrders.length} Failed Order{failedOrders.length !== 1 ? 's' : ''}</p>
|
|
<p className="text-sm text-muted">
|
|
{failedOrders.length} order{failedOrders.length !== 1 ? 's' : ''} failed payment processing.{' '}
|
|
<Link href="/admin/orders" className="text-splash-pink hover:text-splash-pink-dark">
|
|
Review →
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{designApprovals.length > 0 && (
|
|
<div className="border border-splash-pink/40 bg-splash-pink/5 p-4 rounded mt-4">
|
|
<p className="font-medium text-splash-pink mb-2">✏ {designApprovals.length} Design Review{designApprovals.length !== 1 ? 's' : ''}</p>
|
|
<p className="text-sm text-muted">
|
|
{designApprovals.length} design{designApprovals.length !== 1 ? 's' : ''} awaiting approval.{' '}
|
|
<Link href="/admin/designs" className="text-splash-pink hover:text-splash-pink-dark">
|
|
Review →
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div className="space-y-8">
|
|
{/* Top Customers */}
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<h3 className="font-display text-xl mb-4">Top 10 Customers</h3>
|
|
{topCustomers.length === 0 ? (
|
|
<p className="text-sm text-muted">No customers yet</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{topCustomers.map((customer, idx) => (
|
|
<div key={customer.id} className="pb-3 border-b border-line last:border-b-0 last:pb-0">
|
|
<p className="font-medium text-sm">
|
|
{idx + 1}. {customer.name}
|
|
</p>
|
|
<p className="text-xs text-muted">{customer.email}</p>
|
|
<p className="text-xs text-muted mt-1">
|
|
{customer.orderCount} order{customer.orderCount !== 1 ? 's' : ''} · £
|
|
{(customer.totalSpent / 100).toFixed(2)}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="border border-line bg-surface p-6 rounded">
|
|
<h3 className="font-display text-xl mb-4">Quick Actions</h3>
|
|
<div className="space-y-2">
|
|
<Link
|
|
href="/admin/orders"
|
|
className="block w-full text-center bg-clay text-paper px-4 py-2 text-sm font-medium rounded hover:bg-clay-dark"
|
|
>
|
|
View All Orders
|
|
</Link>
|
|
<Link
|
|
href="/admin/customers"
|
|
className="block w-full text-center border border-line text-ink px-4 py-2 text-sm font-medium rounded hover:bg-surface-1"
|
|
>
|
|
Email Customers
|
|
</Link>
|
|
<Link
|
|
href="/admin/designs"
|
|
className="block w-full text-center border border-line text-ink px-4 py-2 text-sm font-medium rounded hover:bg-surface-1"
|
|
>
|
|
Design Reviews
|
|
</Link>
|
|
<Link
|
|
href="/admin/products/new"
|
|
className="block w-full text-center border border-line text-ink px-4 py-2 text-sm font-medium rounded hover:bg-surface-1"
|
|
>
|
|
Add Product
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|