Feature: Add order tracking and shipping status updates

- Add carrier, trackingNumber, and estimatedDeliveryDate fields to Order model
- Update order statuses: PENDING, PAID, PRINTING, PRINTED, SHIPPED, DELIVERED, FAILED
- Add admin UI on order detail page to update tracking information
- Display tracking info on customer account page in order history
- Server action to update order tracking and status

Customers can now see their shipping status and tracking details from
their account page. Admins can update order status and tracking info
from the order detail page.
This commit is contained in:
Andymick
2026-07-18 20:34:19 +01:00
parent f4a80d2a50
commit 440a735b8a
6 changed files with 133 additions and 19 deletions
+31 -16
View File
@@ -90,22 +90,37 @@ export default async function AccountPage() {
) : (
<div className="mt-4 divide-y divide-line border-t border-line">
{orders.map((order) => (
<Link
key={order.id}
href={`/account/orders/${order.id}`}
className="flex flex-wrap items-center justify-between gap-2 py-4 hover:bg-surface"
>
<div>
<p className="text-sm">
{new Date(order.createdAt).toLocaleDateString(undefined, { dateStyle: 'medium' })} ·{' '}
{order.items.length} item{order.items.length === 1 ? '' : 's'}
</p>
<span className={`tag-label mt-1 inline-block rounded-full px-3 py-1 ${statusClasses(order.status)}`}>
{statusLabel(order.status)}
</span>
</div>
<span className="font-mono text-sm">{formatPrice(order.total)}</span>
</Link>
<div key={order.id} className="border-b border-line py-4 last:border-b-0">
<Link
href={`/account/orders/${order.id}`}
className="flex flex-wrap items-center justify-between gap-2 hover:opacity-80"
>
<div>
<p className="text-sm">
{new Date(order.createdAt).toLocaleDateString(undefined, { dateStyle: 'medium' })} ·{' '}
{order.items.length} item{order.items.length === 1 ? '' : 's'}
</p>
<span className={`tag-label mt-1 inline-block rounded-full px-3 py-1 ${statusClasses(order.status)}`}>
{statusLabel(order.status)}
</span>
</div>
<span className="font-mono text-sm">{formatPrice(order.total)}</span>
</Link>
{/* Tracking Information */}
{order.carrier && order.trackingNumber && (
<div className="mt-3 border-t border-line pt-3 text-sm">
<p>
<strong>Tracking:</strong> {order.carrier} · {order.trackingNumber}
</p>
{order.estimatedDeliveryDate && (
<p className="text-muted">
Est. delivery: {new Date(order.estimatedDeliveryDate).toLocaleDateString(undefined, { dateStyle: 'medium' })}
</p>
)}
</div>
)}
</div>
))}
</div>
)}
+67 -1
View File
@@ -1,7 +1,7 @@
import { notFound } from 'next/navigation';
import { prisma } from '@/lib/prisma';
import AdminNav from '@/components/AdminNav';
import { archiveOrder, unarchiveOrder } from '../actions';
import { archiveOrder, unarchiveOrder, updateOrderTracking } from '../actions';
function formatPrice(cents: number) {
return `£${(cents / 100).toFixed(2)}`;
@@ -76,6 +76,72 @@ export default async function OrderDetailPage({ params }: { params: { id: string
)}
</div>
{/* Tracking Information */}
<div className="mt-6 border border-line bg-surface p-6">
<h2 className="mb-4 font-display text-lg">Shipping & Tracking</h2>
<form action={updateOrderTracking} className="space-y-4">
<input type="hidden" name="orderId" value={order.id} />
<div className="grid gap-4 sm:grid-cols-2">
<div>
<label className="tag-label mb-1 block">Status</label>
<select
name="status"
defaultValue={order.status}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
>
<option value="PENDING">PENDING</option>
<option value="PAID">PAID</option>
<option value="PRINTING">PRINTING</option>
<option value="PRINTED">PRINTED</option>
<option value="SHIPPED">SHIPPED</option>
<option value="DELIVERED">DELIVERED</option>
<option value="FAILED">FAILED</option>
</select>
</div>
<div>
<label className="tag-label mb-1 block">Carrier</label>
<input
type="text"
name="carrier"
defaultValue={order.carrier ?? ''}
placeholder="e.g. Royal Mail, DPD, Courier"
className="w-full border border-line bg-paper px-3 py-2 text-sm"
/>
</div>
<div>
<label className="tag-label mb-1 block">Tracking Number</label>
<input
type="text"
name="trackingNumber"
defaultValue={order.trackingNumber ?? ''}
placeholder="e.g. RM123456789GB"
className="w-full border border-line bg-paper px-3 py-2 text-sm"
/>
</div>
<div>
<label className="tag-label mb-1 block">Estimated Delivery Date</label>
<input
type="date"
name="estimatedDeliveryDate"
defaultValue={order.estimatedDeliveryDate ? new Date(order.estimatedDeliveryDate).toISOString().split('T')[0] : ''}
className="w-full border border-line bg-paper px-3 py-2 text-sm"
/>
</div>
</div>
<button
type="submit"
className="bg-clay px-4 py-2 text-sm font-medium text-paper hover:bg-clay-dark"
>
Update Tracking Info
</button>
</form>
</div>
<div className="mt-10 divide-y divide-line border-t border-line">
{order.items.map((item) => {
let hasFrontDesign = false;
+22
View File
@@ -35,3 +35,25 @@ export async function unarchiveOrder(formData: FormData) {
await prisma.order.update({ where: { id }, data: { archived: false } });
redirect('/admin/orders?archived=1');
}
export async function updateOrderTracking(formData: FormData) {
const id = String(formData.get('orderId') ?? '');
const status = String(formData.get('status') ?? 'PENDING');
const carrier = formData.get('carrier') ? String(formData.get('carrier')) : null;
const trackingNumber = formData.get('trackingNumber') ? String(formData.get('trackingNumber')) : null;
const estimatedDeliveryDate = formData.get('estimatedDeliveryDate') ? new Date(String(formData.get('estimatedDeliveryDate'))) : null;
if (!id) return;
await prisma.order.update({
where: { id },
data: {
status,
carrier,
trackingNumber,
estimatedDeliveryDate,
},
});
// TODO: Send email notification to customer when status changes
}