From aa44a23f17216dd1027e4e26ab419ce95cb8f34a Mon Sep 17 00:00:00 2001 From: Andymick Date: Sun, 19 Jul 2026 10:27:02 +0100 Subject: [PATCH] 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 --- src/app/admin/change-password/actions.ts | 2 +- src/app/admin/change-password/page.tsx | 4 +- src/app/admin/dashboard/page.tsx | 235 +++++++++++++++++++++++ src/app/admin/login/actions.ts | 2 +- src/app/admin/login/page.tsx | 2 +- src/app/admin/orders/page.tsx | 2 + 6 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 src/app/admin/dashboard/page.tsx 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;