diff --git a/src/app/admin/change-password/actions.ts b/src/app/admin/change-password/actions.ts
index 36e1cf6..bfae680 100644
--- a/src/app/admin/change-password/actions.ts
+++ b/src/app/admin/change-password/actions.ts
@@ -45,5 +45,5 @@ export async function changeAdminPassword(formData: FormData) {
const username = dbAdmin?.username ?? process.env.ADMIN_USER ?? 'admin';
await createOrUpdateAdminUser(username, hashedPassword);
- redirect('/admin/orders?success=password_changed');
+ redirect('/admin/dashboard?success=password_changed');
}
diff --git a/src/app/admin/change-password/page.tsx b/src/app/admin/change-password/page.tsx
index 04d8896..1d44fe8 100644
--- a/src/app/admin/change-password/page.tsx
+++ b/src/app/admin/change-password/page.tsx
@@ -18,8 +18,8 @@ export default async function ChangeAdminPasswordPage({ searchParams }: { search
return (
-
- ← Back to orders
+
+ ← Back to dashboard
Change admin password
diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx
new file mode 100644
index 0000000..735ec45
--- /dev/null
+++ b/src/app/admin/dashboard/page.tsx
@@ -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
Unauthorized
;
+ }
+
+ // 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 (
+
+ {/* Header */}
+
+
+
+
Admin Dashboard
+
+ ← Back to site
+
+
+
+ Overview of orders, customers, and key metrics
+
+
+
+
+ {/* Main Navigation */}
+
+
+
+ {/* Key Statistics */}
+
+
+
Active Orders
+
{activeOrders.length}
+
Pending or paid
+
+
+
Revenue (Paid)
+
£{(totalRevenue / 100).toFixed(2)}
+
{orders.filter((o) => o.status === 'PAID').length} orders
+
+
+
Delivered
+
{deliveredCount}
+
Completed orders
+
+
+
Design Reviews
+
{designApprovals.length}
+
Awaiting approval
+
+
+
+ {/* Main Content Grid */}
+
+ {/* Active Orders */}
+
+
+
+
Active Orders
+
+ View all →
+
+
+
+ {activeOrders.length === 0 ? (
+
No active orders
+ ) : (
+
+ {activeOrders.slice(0, 8).map((order) => (
+
+
+
+ Order #{order.id.slice(0, 8).toUpperCase()}
+
+
+ {order.email || order.guestName || 'Guest'} · {order.items.length} item
+ {order.items.length !== 1 ? 's' : ''}
+
+
+
+
£{(order.total / 100).toFixed(2)}
+
+ {order.status === 'PENDING' ? 'Awaiting payment' : 'Paid'}
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Alerts */}
+ {failedOrders.length > 0 && (
+
+
⚠ {failedOrders.length} Failed Order{failedOrders.length !== 1 ? 's' : ''}
+
+ {failedOrders.length} order{failedOrders.length !== 1 ? 's' : ''} failed payment processing.{' '}
+
+ Review →
+
+
+
+ )}
+
+ {designApprovals.length > 0 && (
+
+
✏ {designApprovals.length} Design Review{designApprovals.length !== 1 ? 's' : ''}
+
+ {designApprovals.length} design{designApprovals.length !== 1 ? 's' : ''} awaiting approval.{' '}
+
+ Review →
+
+
+
+ )}
+
+
+ {/* Sidebar */}
+
+ {/* Top Customers */}
+
+
Top 10 Customers
+ {topCustomers.length === 0 ? (
+
No customers yet
+ ) : (
+
+ {topCustomers.map((customer, idx) => (
+
+
+ {idx + 1}. {customer.name}
+
+
{customer.email}
+
+ {customer.orderCount} order{customer.orderCount !== 1 ? 's' : ''} · £
+ {(customer.totalSpent / 100).toFixed(2)}
+
+
+ ))}
+
+ )}
+
+
+ {/* Quick Actions */}
+
+
Quick Actions
+
+
+ View All Orders
+
+
+ Email Customers
+
+
+ Design Reviews
+
+
+ Add Product
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/admin/login/actions.ts b/src/app/admin/login/actions.ts
index 0868920..39bc21d 100644
--- a/src/app/admin/login/actions.ts
+++ b/src/app/admin/login/actions.ts
@@ -9,7 +9,7 @@ import { getAdminUser, verifyPassword } from '@/lib/adminAuth';
function safeNext(next: FormDataEntryValue | null) {
const path = String(next ?? '');
- return path.startsWith('/') && !path.startsWith('//') ? path : '/admin/orders';
+ return path.startsWith('/') && !path.startsWith('//') ? path : '/admin/dashboard';
}
export async function loginAdmin(formData: FormData) {
diff --git a/src/app/admin/login/page.tsx b/src/app/admin/login/page.tsx
index 2d070d3..a959d56 100644
--- a/src/app/admin/login/page.tsx
+++ b/src/app/admin/login/page.tsx
@@ -7,7 +7,7 @@ const ERROR_MESSAGES: Record
= {
export default function AdminLoginPage({ searchParams }: { searchParams: { error?: string; next?: string } }) {
const error = searchParams.error ? (ERROR_MESSAGES[searchParams.error] ?? 'Something went wrong.') : null;
- const next = searchParams.next ?? '/admin/orders';
+ const next = searchParams.next ?? '/admin/dashboard';
return (
diff --git a/src/app/admin/orders/page.tsx b/src/app/admin/orders/page.tsx
index 9068711..e9fac03 100644
--- a/src/app/admin/orders/page.tsx
+++ b/src/app/admin/orders/page.tsx
@@ -27,6 +27,8 @@ const SUCCESS_MESSAGES: Record = {
};
export default async function OrdersPage({ searchParams }: { searchParams: { archived?: string; personalised?: string; success?: string } }) {
+ // Handle success messages from other pages
+ const success = searchParams.success ? SUCCESS_MESSAGES[searchParams.success] : null;
await autoArchiveOldOrders();
const success = searchParams.success ? SUCCESS_MESSAGES[searchParams.success] : null;