Feature: Create comprehensive admin dashboard
- 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>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
df4a95b3c8
commit
aa44a23f17
@@ -0,0 +1,235 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user