DashboardPage.tsx 4.77 KB
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import {
  catalog,
  finance,
  inventory,
  partners,
  production,
  purchaseOrders,
  salesOrders,
} from '@/api/client'
import { PageHeader } from '@/components/PageHeader'
import { Loading } from '@/components/Loading'
import { ErrorBox } from '@/components/ErrorBox'
import { useAuth } from '@/auth/AuthContext'

interface DashboardCounts {
  items: number
  partners: number
  locations: number
  salesOrders: number
  purchaseOrders: number
  workOrders: number
  journalEntries: number
  inProgressWorkOrders: number
}

export function DashboardPage() {
  const { username } = useAuth()
  const [counts, setCounts] = useState<DashboardCounts | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    let active = true
    setLoading(true)
    Promise.all([
      catalog.listItems(),
      partners.list(),
      inventory.listLocations(),
      salesOrders.list(),
      purchaseOrders.list(),
      production.listWorkOrders(),
      finance.listJournalEntries(),
      production.shopFloor(),
    ])
      .then(([items, parts, locs, sos, pos, wos, jes, sf]) => {
        if (!active) return
        setCounts({
          items: items.length,
          partners: parts.length,
          locations: locs.length,
          salesOrders: sos.length,
          purchaseOrders: pos.length,
          workOrders: wos.length,
          journalEntries: jes.length,
          inProgressWorkOrders: sf.length,
        })
      })
      .catch((e: unknown) => {
        if (active) setError(e instanceof Error ? e : new Error(String(e)))
      })
      .finally(() => active && setLoading(false))
    return () => {
      active = false
    }
  }, [])

  return (
    <div>
      <PageHeader
        title={`Welcome${username ? ', ' + username : ''}`}
        subtitle="The framework's buy-make-sell loop, end to end through the same Postgres."
      />
      {loading && <Loading />}
      {error && <ErrorBox error={error} />}
      {counts && (
        <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
          <DashboardCard label="Items" value={counts.items} to="/items" />
          <DashboardCard label="Partners" value={counts.partners} to="/partners" />
          <DashboardCard label="Locations" value={counts.locations} to="/locations" />
          <DashboardCard
            label="Work orders in progress"
            value={counts.inProgressWorkOrders}
            to="/shop-floor"
            highlight
          />
          <DashboardCard label="Sales orders" value={counts.salesOrders} to="/sales-orders" />
          <DashboardCard
            label="Purchase orders"
            value={counts.purchaseOrders}
            to="/purchase-orders"
          />
          <DashboardCard label="Work orders" value={counts.workOrders} to="/work-orders" />
          <DashboardCard
            label="Journal entries"
            value={counts.journalEntries}
            to="/journal-entries"
          />
        </div>
      )}
      <div className="mt-8 card p-5 text-sm text-slate-600">
        <h2 className="mb-2 text-base font-semibold text-slate-800">Try the demo</h2>
        <ol className="list-decimal space-y-1 pl-5">
          <li>
            Open a <Link to="/sales-orders" className="text-brand-600 hover:underline">sales order</Link>{' '}
            in DRAFT, click <span className="font-mono">Confirm</span>.
          </li>
          <li>
            With the same sales order CONFIRMED, click <span className="font-mono">Ship</span>.
            The framework atomically debits stock, flips status to SHIPPED, and emits a domain
            event.
          </li>
          <li>
            Watch <Link to="/balances" className="text-brand-600 hover:underline">stock balances</Link>{' '}
            drop, <Link to="/movements" className="text-brand-600 hover:underline">movements</Link>{' '}
            grow by one row, and{' '}
            <Link to="/journal-entries" className="text-brand-600 hover:underline">
              journal entries
            </Link>{' '}
            settle from POSTED to SETTLED — all from the cross-PBC event subscriber in pbc-finance.
          </li>
        </ol>
      </div>
    </div>
  )
}

function DashboardCard({
  label,
  value,
  to,
  highlight = false,
}: {
  label: string
  value: number
  to: string
  highlight?: boolean
}) {
  return (
    <Link
      to={to}
      className={[
        'card flex flex-col gap-1 p-5 transition hover:shadow-md',
        highlight ? 'ring-2 ring-brand-200' : '',
      ].join(' ')}
    >
      <div className="text-xs font-semibold uppercase tracking-wide text-slate-500">{label}</div>
      <div className="text-3xl font-bold text-slate-900">{value}</div>
    </Link>
  )
}