diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index dbb3e5b..c038a7c 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -91,7 +91,9 @@
"Bash(git commit -m 'Add: Shipping selector component and checkout integration *)",
"Bash(node clear-pending-orders.js)",
"mcp__visualize__read_me",
- "mcp__visualize__show_widget"
+ "mcp__visualize__show_widget",
+ "Bash(git branch *)",
+ "Bash(git remote *)"
]
}
}
diff --git a/prisma/migrations/20260719074839_add_collected_by_customer/migration.sql b/prisma/migrations/20260719074839_add_collected_by_customer/migration.sql
new file mode 100644
index 0000000..40862ac
--- /dev/null
+++ b/prisma/migrations/20260719074839_add_collected_by_customer/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Order" ADD COLUMN "collectedByCustomer" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/20260719081419_add_collection_pickup_fields/migration.sql b/prisma/migrations/20260719081419_add_collection_pickup_fields/migration.sql
new file mode 100644
index 0000000..0f89d10
--- /dev/null
+++ b/prisma/migrations/20260719081419_add_collection_pickup_fields/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "Order" ADD COLUMN "isCollectionPickup" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "readyForCollection" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/20260719081905_add_phone_number_to_order/migration.sql b/prisma/migrations/20260719081905_add_phone_number_to_order/migration.sql
new file mode 100644
index 0000000..c728305
--- /dev/null
+++ b/prisma/migrations/20260719081905_add_phone_number_to_order/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Order" ADD COLUMN "phoneNumber" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e378fae..de1a354 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -231,6 +231,9 @@ model Order {
carrier String? @default("Royal Mail") // e.g. "Royal Mail", "DPD", "Courier"
trackingNumber String? // tracking number from carrier
estimatedDeliveryDate DateTime? // estimated delivery date
+ collectedByCustomer Boolean @default(false) // true if order was collected by customer (no shipping)
+ isCollectionPickup Boolean @default(false) // true if customer selected collection at checkout
+ readyForCollection Boolean @default(false) // true when admin marks order as ready for pickup
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -240,6 +243,7 @@ model Order {
guestName String? // full name for guest orders
guestPhone String? // contact phone for guest orders
+ phoneNumber String? // phone number for WhatsApp notifications (collection orders)
items OrderItem[]
}
diff --git a/src/app/account/orders/[id]/page.tsx b/src/app/account/orders/[id]/page.tsx
index 718576f..87e25d3 100644
--- a/src/app/account/orders/[id]/page.tsx
+++ b/src/app/account/orders/[id]/page.tsx
@@ -7,14 +7,16 @@ function formatPrice(cents: number) {
return `£${(cents / 100).toFixed(2)}`;
}
-function statusLabel(status: string) {
- if (status === 'PAID') return 'Confirmed';
+function statusLabel(status: string, trackingNumber?: string | null, collectedByCustomer?: boolean) {
+ if (status === 'DELIVERED') return collectedByCustomer ? 'Collected' : 'Delivered';
+ if (status === 'PAID') return trackingNumber ? 'Dispatched' : 'Awaiting dispatch';
if (status === 'FAILED') return 'Failed';
return 'Processing';
}
-function statusClasses(status: string) {
- if (status === 'PAID') return 'bg-splash-blue/10 text-splash-blue';
+function statusClasses(status: string, trackingNumber?: string | null) {
+ if (status === 'DELIVERED') return 'bg-green-500/10 text-green-600';
+ if (status === 'PAID') return trackingNumber ? 'bg-splash-blue/10 text-splash-blue' : 'bg-splash-orange/10 text-splash-orange';
if (status === 'FAILED') return 'bg-splash-pink/10 text-splash-pink';
return 'bg-splash-orange/10 text-splash-orange';
}
@@ -38,9 +40,32 @@ export default async function AccountOrderDetailPage({ params }: { params: { id:
Order
- {statusLabel(order.status)}
+ {statusLabel(order.status, order.trackingNumber, order.collectedByCustomer)}
+ {order.isCollectionPickup && order.readyForCollection && (
+
+
Ready for Collection
+
✓ Your order is ready for pickup
+
Please collect your order at your earliest convenience
+
+ )}
+
+ {order.trackingNumber && (
+
+
Tracking information
+
+ {order.carrier || 'Royal Mail'}
+
+
{order.trackingNumber}
+ {order.estimatedDeliveryDate && (
+
+ Est. delivery: {new Date(order.estimatedDeliveryDate).toLocaleDateString(undefined, { dateStyle: 'medium' })}
+
+ )}
+
+ )}
+
Placed
diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx
index b22727b..136206f 100644
--- a/src/app/account/page.tsx
+++ b/src/app/account/page.tsx
@@ -10,15 +10,17 @@ function formatPrice(cents: number) {
return `£${(cents / 100).toFixed(2)}`;
}
-function statusLabel(status: string) {
- if (status === 'PAID') return 'Confirmed';
+function statusLabel(status: string, trackingNumber?: string | null, collectedByCustomer?: boolean) {
+ if (status === 'DELIVERED') return collectedByCustomer ? 'Collected' : 'Delivered';
+ if (status === 'PAID') return trackingNumber ? 'Dispatched' : 'Awaiting dispatch';
if (status === 'FAILED') return 'Failed';
if (status === 'PENDING') return 'Waiting payment';
return 'Processing';
}
-function statusClasses(status: string) {
- if (status === 'PAID') return 'bg-splash-blue/10 text-splash-blue';
+function statusClasses(status: string, trackingNumber?: string | null) {
+ if (status === 'DELIVERED') return 'bg-green-500/10 text-green-600';
+ if (status === 'PAID') return trackingNumber ? 'bg-splash-blue/10 text-splash-blue' : 'bg-splash-orange/10 text-splash-orange';
if (status === 'FAILED') return 'bg-splash-pink/10 text-splash-pink';
return 'bg-splash-orange/10 text-splash-orange';
}
@@ -114,13 +116,21 @@ export default async function AccountPage() {
{new Date(order.createdAt).toLocaleDateString(undefined, { dateStyle: 'medium' })} ·{' '}
{order.items.length} item{order.items.length === 1 ? '' : 's'}
-
- {statusLabel(order.status)}
+
+ {statusLabel(order.status, order.trackingNumber, order.collectedByCustomer)}
{formatPrice(order.total)}
+ {/* Collection Ready Notification */}
+ {order.isCollectionPickup && order.readyForCollection && (
+
+
✓ Your order is ready for collection
+
Please come collect your order at your earliest convenience
+
+ )}
+
{/* Tracking Information */}
{order.carrier && order.trackingNumber && (
diff --git a/src/app/admin/gallery/actions.ts b/src/app/admin/gallery/actions.ts
index 40dfe8d..ad3378e 100644
--- a/src/app/admin/gallery/actions.ts
+++ b/src/app/admin/gallery/actions.ts
@@ -59,13 +59,14 @@ export async function deleteGalleryPhoto(formData: FormData) {
export async function createGalleryCategory(formData: FormData) {
const name = String(formData.get('name') ?? '').trim();
+ const redirectTo = String(formData.get('redirectTo') ?? '/admin/gallery/categories').trim();
if (!name) throw new Error('Category name is required.');
const slug = await uniqueSlug(slugify(name));
await prisma.galleryCategory.create({ data: { name, slug } });
- redirect('/admin/gallery/categories');
+ redirect(redirectTo);
}
export async function deleteGalleryCategory(formData: FormData) {
diff --git a/src/app/admin/gallery/categories/new/page.tsx b/src/app/admin/gallery/categories/new/page.tsx
index a6fcf12..d5e00dc 100644
--- a/src/app/admin/gallery/categories/new/page.tsx
+++ b/src/app/admin/gallery/categories/new/page.tsx
@@ -1,7 +1,13 @@
import AdminNav from '@/components/AdminNav';
import { createGalleryCategory } from '../../actions';
-export default function NewGalleryCategoryPage() {
+export default function NewGalleryCategoryPage({
+ searchParams,
+}: {
+ searchParams: { redirectTo?: string };
+}) {
+ const redirectTo = searchParams.redirectTo || '/admin/gallery/categories';
+
return (
@@ -11,6 +17,7 @@ export default function NewGalleryCategoryPage() {
+
+
+
+