Skip to contentVibraUI

Fulfillment headline

Parcels in transit, delivered this week, open exceptions and mean transit time, each against the span before; reads fulfillmentStats().

Preview

Install

npx shadcn@latest add @vibra/widget-ecommerce-fulfillment-fulfillment-stats

Needs the @vibra registry in your components.json — set it up once.

Source

app/ecommerce/fulfillment/components/fulfillment-stats.tsx
import { formatNumber } from "@/lib/format"
import { StatCard } from "@/components/ui/stat-card"
import { StatCardGroup } from "@/components/ui/stat-card-group"

import { type FulfillmentStat } from "../data"

/** Hours, printed the way a warehouse says them: "31h", or "1.3d" past two days. */
function hours(value: number): string {
  if (value < 48) return `${formatNumber(value, { maximumFractionDigits: 0 })}h`
  return `${formatNumber(value / 24, { maximumFractionDigits: 1 })}d`
}

/** The four numbers a shift starts on. */
export function FulfillmentStats({ stats }: { stats: FulfillmentStat[] }) {
  return (
    <StatCardGroup data-widget="widget-ecommerce-fulfillment-fulfillment-stats" columns={4}>
      {stats.map((stat) => (
        <StatCard
          key={stat.key}
          label={stat.label}
          value={stat.kind === "hours" ? hours(stat.value) : formatNumber(stat.value)}
          delta={stat.delta}
          deltaFormat="percent"
          positiveIsGood={stat.positiveIsGood}
          description={stat.description}
        />
      ))}
    </StatCardGroup>
  )
}
app/ecommerce/fulfillment/data.ts
/**
 * What this page reads. Every row is a `db.shipments` record — a parcel against
 * an order, four stages of which the ones already reached carry a timestamp, a
 * carrier, a service, and an exception where the warehouse raised one. Nothing
 * is invented: a number on this page is a count or a mean over those rows.
 * "Now" is `REFERENCE_DATE`, and every bucket boundary is UTC.
 */
import { getInitials } from "@/lib/format"
import {
  REFERENCE_DATE,
  SHIPMENT_STAGES,
  daysAgo,
  db,
  type Member,
  type Shipment,
  type ShipmentException,
  type ShipmentStageName,
} from "@/lib/sample-data"
import { type StageProgressStage } from "@/components/ui/stage-progress"

import { monthBuckets, weekBuckets, type Bucket } from "./buckets"
import { SERVICE_LABELS } from "./vocabulary"

// Read per call, never held at module scope: a parcel marked shipped on the
// order's own page is on this board the next time it is asked for.
const shipments = (): Shipment[] => db.shipments.all()

const HOUR_MS = 3_600_000

/** A parcel that has not reached the customer yet. */
const flying = (row: Shipment) => row.stage !== "delivered"

/** When a stage was stamped, if it has been. */
const stampOf = (row: Shipment, name: ShipmentStageName): Date | undefined =>
  row.stages.find((stage) => stage.name === name)?.at

const inWindow = (at: Date, from: Date, to: Date) =>
  at.getTime() >= from.getTime() && at.getTime() < to.getTime()

/** The change from `was` to `now` as a ratio; 0 when there was nothing to grow from. */
function growth(now: number, was: number): number {
  if (was === 0) return 0
  return (now - was) / was
}

export type FulfillmentStat = {
  key: string
  label: string
  value: number
  /** "count" prints as a whole number, "hours" as a duration. */
  kind: "count" | "hours"
  description: string
  /** Left out where there is nothing honest to compare against. */
  delta?: number
  /** False where a rise is the bad news. */
  positiveIsGood: boolean
}

/** Parcels delivered inside a span. */
const deliveredBetween = (from: Date, to: Date) =>
  shipments().filter((row) => {
    const at = stampOf(row, "delivered")
    return at !== undefined && inWindow(at, from, to)
  })

/** Mean hours from the carrier taking a parcel to the customer having it. */
function meanTransitHours(rows: Shipment[]): number {
  const spans = rows
    .map((row) => {
      const shipped = stampOf(row, "shipped")
      const delivered = stampOf(row, "delivered")
      return shipped && delivered ? delivered.getTime() - shipped.getTime() : null
    })
    .filter((span): span is number => span !== null)
  if (spans.length === 0) return 0
  return spans.reduce((sum, span) => sum + span, 0) / spans.length / HOUR_MS
}

/** The four headline numbers a warehouse morning starts on. */
export function fulfillmentStats(): FulfillmentStat[] {
  const thisWeek = deliveredBetween(daysAgo(7), REFERENCE_DATE)
  const lastWeek = deliveredBetween(daysAgo(14), daysAgo(7))
  const transitNow = meanTransitHours(deliveredBetween(daysAgo(30), REFERENCE_DATE))
  const transitBefore = meanTransitHours(deliveredBetween(daysAgo(60), daysAgo(30)))

  return [
    {
      key: "in-transit",
      label: "In transit",
      value: shipments().filter(flying).length,
      kind: "count",
      description: "Picking, packing, or with a carrier",
      positiveIsGood: true,
    },
    {
      key: "delivered",
      label: "Delivered this week",
      value: thisWeek.length,
      kind: "count",
      description: "vs the week before",
      delta: growth(thisWeek.length, lastWeek.length),
      positiveIsGood: true,
    },
    {
      key: "exceptions",
      label: "Open exceptions",
      value: shipments().filter((row) => flying(row) && row.exception).length,
      kind: "count",
      description: "Parcels stopped and waiting on someone",
      positiveIsGood: false,
    },
    {
      key: "transit",
      label: "Mean transit",
      value: transitNow,
      kind: "hours",
      description: "Carrier handover to doorstep, last 30 days",
      delta: growth(transitNow, transitBefore),
      // A longer journey is worse news, so the arrow points the other way.
      positiveIsGood: false,
    },
  ]
}

/**
 * One bucket of the throughput chart. The index key is the bucket's own name —
 * `week` or `month` — so the chart's spoken summary reads "by week" rather than
 * "by label"; every other key is a service's count.
 */
export type ThroughputPoint = Record<string, number | string>

/** Parcels handed to a carrier in each bucket, one count per service. */
function bucketed(buckets: Bucket[], index: string): ThroughputPoint[] {
  return buckets.map((bucket) => {
    const point: ThroughputPoint = { [index]: bucket.label }
    for (const service of Object.keys(SERVICE_LABELS)) point[service] = 0

    for (const row of shipments()) {
      const shipped = stampOf(row, "shipped")
      if (!shipped || !inWindow(shipped, bucket.start, bucket.end)) continue
      point[row.service] = (point[row.service] as number) + 1
    }
    return point
  })
}

/**
 * Throughput by service, at both bucket sizes the reader can ask for. Both are
 * computed here, on the server, so switching between them costs no round trip
 * and no second read of the store.
 */
export function throughput(): { weeks: ThroughputPoint[]; months: ThroughputPoint[] } {
  return { weeks: bucketed(weekBuckets(12), "week"), months: bucketed(monthBuckets(6), "month") }
}

export type Parcel = {
  id: string
  orderNumber: string
  carrier: string
  service: Shipment["service"]
  destination: string
  tracking?: string
  stage: ShipmentStageName
  /** Every stage in order, with the state the bar draws it in. */
  stages: StageProgressStage[]
  /** When the parcel reached the stage it is on now. */
  since: Date
  /** Set where the parcel is stopped, which is what makes its stage blocked. */
  exception?: ShipmentException
}

/** A parcel the warehouse has to do something about: its exception is not optional. */
export type StuckParcel = Parcel & { exception: ShipmentException }

/** What each stage is called on the page. */
const STAGE_LABELS: Record<ShipmentStageName, string> = {
  picking: "Picking",
  packing: "Packing",
  shipped: "Shipped",
  delivered: "Delivered",
}

/** All four stages, each in the state the parcel's own progress puts it in. */
export function stageBars(row: Shipment): StageProgressStage[] {
  const reached = SHIPMENT_STAGES.indexOf(row.stage)

  return SHIPMENT_STAGES.map((name, index) => ({
    id: name,
    label: STAGE_LABELS[name],
    state:
      index < reached
        ? ("done" as const)
        : index > reached
          ? ("pending" as const)
          : // The stage the parcel sits on: blocked when something stopped it
            // there, done when the journey is over, otherwise still running.
            row.exception
            ? ("blocked" as const)
            : row.stage === "delivered"
              ? ("done" as const)
              : ("active" as const),
  }))
}

function toParcel(row: Shipment): Parcel {
  return {
    id: row.id,
    orderNumber: row.orderNumber,
    carrier: row.carrier,
    service: row.service,
    destination: row.destination,
    ...(row.tracking ? { tracking: row.tracking } : {}),
    stage: row.stage,
    stages: stageBars(row),
    since: stampOf(row, row.stage) ?? REFERENCE_DATE,
    ...(row.exception
      ? {
          exception: {
            code: row.exception.code,
            message: row.exception.message,
            raisedAt: row.exception.raisedAt,
          },
        }
      : {}),
  }
}

/** How many parcels the stage board holds: the oldest work, not all of it. */
export const IN_FLIGHT_SHOWN = 8

/**
 * The parcels still moving that have been sitting at their current stage
 * longest — the ones a shift picks up first — oldest first. A parcel with an
 * exception is not moving at all: the exceptions queue owns it, and listing
 * it here as well would fill the board with the same rows twice.
 */
export function inFlightParcels(): Parcel[] {
  return shipments().filter((row) => flying(row) && !row.exception)
    .map(toParcel)
    .sort((a, b) => a.since.getTime() - b.since.getTime())
    .slice(0, IN_FLIGHT_SHOWN)
}

export type CarrierLoad = { carrier: string; inFlight: number; delivered: number }

/** What each carrier is holding now, heaviest first. */
export function carrierLoad(): CarrierLoad[] {
  const load = new Map<string, CarrierLoad>()

  for (const row of shipments()) {
    const entry = load.get(row.carrier) ?? { carrier: row.carrier, inFlight: 0, delivered: 0 }
    if (flying(row)) entry.inFlight += 1
    else entry.delivered += 1
    load.set(row.carrier, entry)
  }

  return [...load.values()].sort((a, b) => b.inFlight - a.inFlight)
}

function allOpenExceptions(): StuckParcel[] {
  return shipments().filter((row) => flying(row) && row.exception)
    .map(toParcel)
    .filter((parcel): parcel is StuckParcel => parcel.exception !== undefined)
    .sort((a, b) => b.exception.raisedAt.getTime() - a.exception.raisedAt.getTime())
}

/** How many exceptions the queue shows before its footer says how many more there are. */
export const EXCEPTIONS_SHOWN = 8

/** The newest problems, capped the way the in-flight board caps its own list. */
export function openExceptions(): StuckParcel[] {
  return allOpenExceptions().slice(0, EXCEPTIONS_SHOWN)
}

/** How many exceptions are open in total — what the queue's footer counts the shown rows against. */
export function openExceptionsCount(): number {
  return allOpenExceptions().length
}

/** The bell's contents: the newest notifications, unread first in the panel. */
export function shellNotifications() {
  return db.notifications
    .all()
    .sort((a, b) => b.at.getTime() - a.at.getTime())
    .slice(0, 6)
    .map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}

function ownerRow(): Member {
  return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}

/** The person looking at the page: whoever owns this workspace. */
export function currentUser() {
  const owner = ownerRow()
  return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}

// Fixed to UTC so the line reads the same wherever the page is rendered.
const UPDATED_AT = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeStyle: "short",
  hourCycle: "h23",
  timeZone: "UTC",
})

/** How many parcels the book holds, and when it was last collected. */
export function lastUpdated(): string {
  return `${shipments().length} parcels · updated ${UPDATED_AT.format(REFERENCE_DATE)} UTC`
}
app/ecommerce/fulfillment/vocabulary.ts
/**
 * The words this page is written in, and nothing else.
 *
 * `data.ts` reads `db` at module scope, so a value a client island imports from
 * it would drag the whole sample-data store into the browser. The service and
 * exception names, and the one function that says how long a parcel has been
 * standing still, have no rows behind them — and nothing here imports a value
 * from the store, which is the whole point of the file.
 */
import { type ShipmentException } from "@/lib/sample-data"

/** What each shipping service is called on the page, in the order it is ranked. */
export const SERVICE_LABELS = {
  standard: "Standard",
  express: "Express",
  overnight: "Overnight",
} as const

/** How long a parcel has been sitting somewhere, in the roundest true words. */
export function sinceLabel(from: Date, now: Date): string {
  const hours = Math.max(0, Math.round((now.getTime() - from.getTime()) / 3_600_000))
  if (hours < 1) return "under an hour"
  if (hours < 48) return `${hours}h`
  return `${Math.round(hours / 24)}d`
}

/** What each exception code is called where a reader has to act on it. */
export const EXCEPTION_LABELS: Record<ShipmentException["code"], string> = {
  address: "Address incomplete",
  customs: "Held at customs",
  damaged: "Damaged in transit",
  delayed: "Late at the hub",
  missing_label: "Label never printed",
}
app/ecommerce/fulfillment/buckets.ts
/**
 * The calendar this page's throughput chart is bucketed on: whole weeks from
 * Monday, and whole calendar months, both in UTC and both measured back from
 * `REFERENCE_DATE`. Nothing here reads `db` or the clock.
 */
import { REFERENCE_DATE } from "@/lib/sample-data"

const DAY_MS = 86_400_000

/** One bucket: the half-open span it covers, and what it is called. */
export type Bucket = { start: Date; end: Date; label: string }

// Fixed to UTC: a bucket named for the day it starts on has to be named the
// same wherever the page renders.
const WEEK_LABEL = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  timeZone: "UTC",
})
const MONTH_LABEL = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" })

/** Weeks back to front: twelve buckets, Monday to Monday, in UTC. */
export function weekBuckets(count: number): Bucket[] {
  // Monday of the week REFERENCE_DATE falls in, at midnight UTC.
  const day = REFERENCE_DATE.getUTCDay()
  const mondayOffset = (day + 6) % 7
  const thisMonday = Date.UTC(
    REFERENCE_DATE.getUTCFullYear(),
    REFERENCE_DATE.getUTCMonth(),
    REFERENCE_DATE.getUTCDate() - mondayOffset
  )

  return Array.from({ length: count }, (_, index) => {
    const start = new Date(thisMonday - (count - 1 - index) * 7 * DAY_MS)
    return {
      start,
      end: new Date(start.getTime() + 7 * DAY_MS),
      label: WEEK_LABEL.format(start),
    }
  })
}

/** Calendar months back to front, in UTC. */
export function monthBuckets(count: number): Bucket[] {
  const year = REFERENCE_DATE.getUTCFullYear()
  const month = REFERENCE_DATE.getUTCMonth()

  return Array.from({ length: count }, (_, index) => {
    const start = new Date(Date.UTC(year, month - (count - 1 - index), 1))
    return {
      start,
      end: new Date(Date.UTC(year, month - (count - 2 - index), 1)),
      label: MONTH_LABEL.format(start),
    }
  })
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the Fulfillment page