147 lines
5.8 KiB
TypeScript
147 lines
5.8 KiB
TypeScript
import { notFound } from 'next/navigation';
|
|
import { prisma } from '@/lib/prisma';
|
|
import AdminNav from '@/components/AdminNav';
|
|
import { archiveOrder, unarchiveOrder } from '../actions';
|
|
|
|
function formatPrice(cents: number) {
|
|
return `£${(cents / 100).toFixed(2)}`;
|
|
}
|
|
|
|
export default async function OrderDetailPage({ params }: { params: { id: string } }) {
|
|
const order = await prisma.order.findUnique({
|
|
where: { id: params.id },
|
|
include: { items: true },
|
|
});
|
|
if (!order) notFound();
|
|
|
|
const shipping = order.shippingAddress ? JSON.parse(order.shippingAddress) : null;
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl px-6 py-12">
|
|
<AdminNav />
|
|
<div className="flex items-baseline justify-between gap-3">
|
|
<h1 className="font-display text-3xl">Order</h1>
|
|
<div className="flex items-center gap-3">
|
|
{order.archived && <span className="tag-label">Archived</span>}
|
|
<form action={order.archived ? unarchiveOrder : archiveOrder}>
|
|
<input type="hidden" name="id" value={order.id} />
|
|
<button type="submit" className="tag-label border border-line px-3 py-1.5 hover:border-clay hover:text-clay">
|
|
{order.archived ? 'Unarchive' : 'Archive'}
|
|
</button>
|
|
</form>
|
|
<span
|
|
className={`tag-label rounded-full px-3 py-1 ${
|
|
order.status === 'PAID'
|
|
? 'bg-splash-blue/10 text-splash-blue'
|
|
: order.status === 'FAILED'
|
|
? 'bg-splash-pink/10 text-splash-pink'
|
|
: 'bg-splash-orange/10 text-splash-orange'
|
|
}`}
|
|
>
|
|
{order.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 grid gap-6 border border-line bg-surface p-6 sm:grid-cols-2">
|
|
<div>
|
|
<p className="tag-label mb-1">Customer</p>
|
|
<p className="text-sm">{order.email ?? 'Not yet available'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="tag-label mb-1">Placed</p>
|
|
<p className="text-sm">
|
|
{new Date(order.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })}
|
|
</p>
|
|
</div>
|
|
{shipping && (
|
|
<div className="sm:col-span-2">
|
|
<p className="tag-label mb-1">Shipping address</p>
|
|
<p className="text-sm">
|
|
{shipping.name}
|
|
<br />
|
|
{[shipping.address?.line1, shipping.address?.line2].filter(Boolean).join(', ')}
|
|
<br />
|
|
{[shipping.address?.city, shipping.address?.postal_code].filter(Boolean).join(', ')}
|
|
<br />
|
|
{shipping.address?.country}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-10 divide-y divide-line border-t border-line">
|
|
{order.items.map((item) => {
|
|
const design = JSON.parse(item.designJson) as { front: unknown[]; back: unknown[] };
|
|
const hasFrontDesign = design.front.length > 0;
|
|
const hasBackDesign = design.back.length > 0;
|
|
const frontPlacement = item.placementPreviewUrl || item.designPreviewUrl;
|
|
return (
|
|
<div key={item.id} className="py-8">
|
|
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
<p className="font-display text-xl">{item.productName}</p>
|
|
<p className="text-sm text-muted">
|
|
{item.color}
|
|
{item.size ? `, ${item.size}` : ''} · qty {item.quantity} ·{' '}
|
|
{formatPrice(item.unitPrice * item.quantity)}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-4 grid gap-6 sm:grid-cols-2">
|
|
<div>
|
|
<img
|
|
src={frontPlacement || undefined}
|
|
alt={`${item.productName} — where the design sits`}
|
|
className="w-full max-w-md border border-line bg-paper object-contain"
|
|
/>
|
|
<div className="mt-2 flex items-center justify-between">
|
|
<p className="tag-label">{item.placementPreviewUrl ? 'On product — front' : 'Front'}</p>
|
|
{hasFrontDesign && (
|
|
<a
|
|
href={item.designPreviewUrl}
|
|
download={`order-${order.id}-${item.id}-front.png`}
|
|
className="text-xs text-clay hover:text-clay-dark"
|
|
>
|
|
Download print file
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{item.placementPreviewUrlBack && (
|
|
<div>
|
|
<img
|
|
src={item.placementPreviewUrlBack}
|
|
alt={`${item.productName} back — where the design sits`}
|
|
className="w-full max-w-md border border-line bg-paper object-contain"
|
|
/>
|
|
<div className="mt-2 flex items-center justify-between">
|
|
<p className="tag-label">On product — back</p>
|
|
{hasBackDesign && item.designPreviewUrlBack && (
|
|
<a
|
|
href={item.designPreviewUrlBack}
|
|
download={`order-${order.id}-${item.id}-back.png`}
|
|
className="text-xs text-clay hover:text-clay-dark"
|
|
>
|
|
Download print file
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end">
|
|
<div className="flex items-baseline gap-3">
|
|
<span className="tag-label">Total</span>
|
|
<span className="font-mono text-xl">{formatPrice(order.total)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|