diff --git a/src/components/admin/Dashboard/index.tsx b/src/components/admin/Dashboard/index.tsx
index 8ec5c10..1008552 100644
--- a/src/components/admin/Dashboard/index.tsx
+++ b/src/components/admin/Dashboard/index.tsx
@@ -8,6 +8,7 @@ import { IconAverage, IconOrders, IconProducts, IconRevenue, IconStock } from '.
import { KpiStrip, type Kpi } from '../KpiStrip'
import { OrderHealth } from '../OrderHealth'
import { PopularProducts, type PopularProduct } from '../PopularProducts'
+import { RecentOrders } from '../RecentOrders'
import { RevenueTrend } from '../RevenueTrend'
import { DEFAULT_CURRENCY_CODE } from '@/currencies'
import {
@@ -147,6 +148,7 @@ async function getOverview(filters: DashboardFilters) {
products: productCount.totalDocs,
statusCounts: orderData.statusCounts,
popular,
+ recentOrders: orderData.recentOrders,
lowStock,
window,
}
@@ -234,6 +236,8 @@ export const Dashboard: React.FC<{
+
+
(result: unknown): T[] => {
if (typeof result !== 'object' || result === null || !('rows' in result)) return []
@@ -77,6 +96,17 @@ export const attentionCountsFrom = (result: unknown): AttentionCounts => {
}
}
+export const recentOrdersFrom = (result: unknown): RecentOrder[] =>
+ rowsFrom(result).map((row) => ({
+ createdAt: new Date(row.created_at).toISOString(),
+ currency: row.currency || 'USD',
+ customer: row.customer || 'Guest customer',
+ fulfillmentStatus: row.fulfillment_status || 'processing',
+ id: row.id,
+ paymentStatus: row.payment_status || 'not_recorded',
+ total: amount(row.amount),
+ }))
+
const emptySales = (days: number): SalesAggregate => ({
averageOrderValue: 0,
dailyNetSales: new Array(days).fill(0),
@@ -166,7 +196,7 @@ export async function getDashboardOrderData({
ORDER BY "day"
`)
- const [statusResult, attentionResult, popularResult] = await Promise.all([
+ const [statusResult, attentionResult, popularResult, recentResult] = await Promise.all([
payload.db.drizzle.execute(sql`
SELECT COALESCE("status"::text, 'processing') AS "status", COUNT(*) AS "count"
FROM "orders"
@@ -231,6 +261,32 @@ export async function getDashboardOrderData({
ORDER BY "units" DESC
LIMIT 5
`),
+ payload.db.drizzle.execute(sql`
+ SELECT
+ "orders"."id",
+ COALESCE(
+ NULLIF("users"."name", ''),
+ NULLIF("orders"."customer_email", ''),
+ NULLIF("users"."email", ''),
+ 'Guest customer'
+ ) AS "customer",
+ COALESCE("orders"."amount", 0) AS "amount",
+ COALESCE("orders"."currency"::text, ${currency}) AS "currency",
+ COALESCE("orders"."status"::text, 'processing') AS "fulfillment_status",
+ "latest_transaction"."status"::text AS "payment_status",
+ "orders"."created_at"
+ FROM "orders"
+ LEFT JOIN "users" ON "users"."id" = "orders"."customer_id"
+ LEFT JOIN LATERAL (
+ SELECT "transactions"."status"
+ FROM "transactions"
+ WHERE "transactions"."order_id" = "orders"."id"
+ ORDER BY "transactions"."updated_at" DESC, "transactions"."id" DESC
+ LIMIT 1
+ ) AS "latest_transaction" ON TRUE
+ ORDER BY "orders"."created_at" DESC, "orders"."id" DESC
+ LIMIT 5
+ `),
])
const current = emptySales(days)
@@ -276,6 +332,7 @@ export async function getDashboardOrderData({
productID: row.product_id,
units: amount(row.units),
})),
+ recentOrders: recentOrdersFrom(recentResult),
statusCounts,
}
}
diff --git a/src/components/admin/RecentOrders/index.scss b/src/components/admin/RecentOrders/index.scss
new file mode 100644
index 0000000..511e248
--- /dev/null
+++ b/src/components/admin/RecentOrders/index.scss
@@ -0,0 +1,166 @@
+.recent-orders {
+ padding: 1.15rem 1.25rem;
+ border: 1px solid var(--theme-elevation-100);
+ border-radius: var(--style-radius-s, 3px);
+ background: var(--theme-elevation-0);
+
+ &__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 0.85rem;
+
+ h3 {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 600;
+ }
+
+ a {
+ color: var(--theme-elevation-650);
+ font-size: 0.8rem;
+ text-decoration: none;
+
+ &:hover {
+ color: var(--theme-elevation-900);
+ }
+ }
+ }
+
+ &__icon {
+ flex: 0 0 auto;
+ color: var(--theme-elevation-500);
+ }
+
+ &__table {
+ display: grid;
+ }
+
+ &__labels,
+ &__row {
+ display: grid;
+ grid-template-columns: 0.65fr minmax(8rem, 1.7fr) 0.9fr 1fr 1fr 0.8fr;
+ gap: 1rem;
+ align-items: center;
+ }
+
+ &__labels {
+ padding: 0 0.7rem 0.45rem;
+ color: var(--theme-elevation-450);
+ font-size: 0.72rem;
+ }
+
+ &__row {
+ min-width: 0;
+ padding: 0.68rem 0.7rem;
+ border-radius: var(--style-radius-s, 3px);
+ color: var(--theme-elevation-700);
+ font-size: 0.82rem;
+ text-decoration: none;
+ transition: background-color 120ms ease;
+
+ &:nth-child(even) {
+ background: var(--theme-elevation-50);
+ }
+
+ &:hover {
+ background: var(--theme-elevation-100);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--theme-elevation-700);
+ outline-offset: 1px;
+ }
+
+ strong,
+ time {
+ color: var(--theme-elevation-850);
+ font-variant-numeric: tabular-nums;
+ }
+ }
+
+ &__customer {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &__money {
+ color: var(--theme-elevation-850);
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ }
+
+ &__status {
+ color: var(--theme-elevation-650);
+
+ &--succeeded {
+ color: var(--theme-success-600, #16803b);
+ }
+
+ &--failed {
+ color: var(--theme-error-500, #b42318);
+ }
+
+ &--pending,
+ &--processing {
+ color: var(--theme-warning-600, #9a6700);
+ }
+ }
+
+ &__empty {
+ margin: 0;
+ color: var(--theme-elevation-500);
+ }
+
+ @media (max-width: 68rem) {
+ &__labels {
+ display: none;
+ }
+
+ &__table {
+ gap: 0.4rem;
+ }
+
+ &__row {
+ grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr) auto;
+ gap: 0.25rem 0.75rem;
+ background: var(--theme-elevation-50);
+
+ strong {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .recent-orders__customer {
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .recent-orders__money {
+ grid-column: 3;
+ grid-row: 1;
+ }
+
+ .recent-orders__status--payment {
+ grid-column: 1;
+ grid-row: 2;
+ }
+
+ .recent-orders__status--fulfilment {
+ grid-column: 2;
+ grid-row: 2;
+ }
+
+ time {
+ grid-column: 3;
+ grid-row: 2;
+ }
+ }
+ }
+}
diff --git a/src/components/admin/RecentOrders/index.tsx b/src/components/admin/RecentOrders/index.tsx
new file mode 100644
index 0000000..bf776e2
--- /dev/null
+++ b/src/components/admin/RecentOrders/index.tsx
@@ -0,0 +1,84 @@
+import Link from 'next/link'
+import React from 'react'
+
+import type { RecentOrder } from '../Dashboard/queries'
+import { IconOrders } from '../icons'
+
+import './index.scss'
+
+type Props = {
+ adminPath: string
+ orders: RecentOrder[]
+ timeZone: string
+}
+
+const money = (minor: number, currency: string) =>
+ new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(minor / 100)
+
+const words = (value: string) =>
+ value
+ .replaceAll('_', ' ')
+ .replace(/\b\w/g, (letter) => letter.toUpperCase())
+
+const fulfillmentLabel = (status: string) => {
+ if (status === 'processing') return 'Unfulfilled'
+ if (status === 'completed') return 'Fulfilled'
+ if (status === 'refunded') return 'Refunded'
+ if (status === 'cancelled') return 'Cancelled'
+ return words(status)
+}
+
+export const RecentOrders: React.FC = ({ adminPath, orders, timeZone }) => (
+
+
+
+
+ Recent orders
+
+ View all
+
+
+ {orders.length === 0 ? (
+ New orders will appear here.
+ ) : (
+
+
+ Order
+ Customer
+ Total
+ Payment
+ Fulfilment
+ Date
+
+ {orders.map((order) => (
+
+
#{order.id}
+
{order.customer}
+
+ {money(order.total, order.currency)}
+
+
+ {words(order.paymentStatus)}
+
+
+ {fulfillmentLabel(order.fulfillmentStatus)}
+
+
+
+ ))}
+
+ )}
+
+)
diff --git a/tests/unit/recent-orders.unit.spec.ts b/tests/unit/recent-orders.unit.spec.ts
new file mode 100644
index 0000000..5fb19ac
--- /dev/null
+++ b/tests/unit/recent-orders.unit.spec.ts
@@ -0,0 +1,57 @@
+import { describe, expect, it } from 'vitest'
+
+import { recentOrdersFrom } from '@/components/admin/Dashboard/queries'
+
+describe('recent dashboard orders', () => {
+ it('normalizes bounded order rows for display', () => {
+ expect(
+ recentOrdersFrom({
+ rows: [
+ {
+ amount: '7600',
+ created_at: '2026-09-27T10:00:00.000Z',
+ currency: 'GBP',
+ customer: 'Trust Jamin',
+ fulfillment_status: 'processing',
+ id: 319,
+ payment_status: 'succeeded',
+ },
+ ],
+ }),
+ ).toEqual([
+ {
+ createdAt: '2026-09-27T10:00:00.000Z',
+ currency: 'GBP',
+ customer: 'Trust Jamin',
+ fulfillmentStatus: 'processing',
+ id: 319,
+ paymentStatus: 'succeeded',
+ total: 7600,
+ },
+ ])
+ })
+
+ it('labels missing legacy payment data without pretending it succeeded', () => {
+ expect(
+ recentOrdersFrom({
+ rows: [
+ {
+ amount: null,
+ created_at: new Date('2026-09-27T10:00:00.000Z'),
+ currency: null,
+ customer: null,
+ fulfillment_status: null,
+ id: 1,
+ payment_status: null,
+ },
+ ],
+ })[0],
+ ).toMatchObject({
+ currency: 'USD',
+ customer: 'Guest customer',
+ fulfillmentStatus: 'processing',
+ paymentStatus: 'not_recorded',
+ total: 0,
+ })
+ })
+})