Files
craft2prints/src/app/admin/dashboard/page.tsx
T
AndymickandClaude Haiku 4.5 aa44a23f17 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>
2026-07-19 10:27:02 +01:00

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>
);
}