DashboardPage.tsx 5.87 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">Getting started</h2>
        <p className="mb-3 text-slate-500">
          The framework's buy-make-sell loop, end to end.
        </p>
        <ol className="list-decimal space-y-2 pl-5">
          <li>
            <strong>Set up master data</strong> — create{' '}
            <Link to="/items/new" className="text-brand-600 hover:underline">items</Link>,{' '}
            <Link to="/partners/new" className="text-brand-600 hover:underline">partners</Link>, and{' '}
            <Link to="/locations/new" className="text-brand-600 hover:underline">locations</Link>.
            Then{' '}
            <Link to="/balances/adjust" className="text-brand-600 hover:underline">adjust stock</Link>{' '}
            to set opening balances.
          </li>
          <li>
            <strong>Create a sales order</strong> —{' '}
            <Link to="/sales-orders/new" className="text-brand-600 hover:underline">new order</Link>{' '}
            with line items. Confirm it — the system auto-generates production work orders and
            posts an AR journal entry with double-entry lines (DR Accounts Receivable, CR Revenue).
          </li>
          <li>
            <strong>Walk the work order</strong> — start it, walk routing operations on the{' '}
            <Link to="/shop-floor" className="text-brand-600 hover:underline">Shop Floor</Link>,
            then complete it. Materials are consumed, finished goods credited.
          </li>
          <li>
            <strong>Ship the sales order</strong> — stock leaves the warehouse, the AR journal
            entry settles. View the ledger in{' '}
            <Link to="/movements" className="text-brand-600 hover:underline">Movements</Link>{' '}
            and double-entry lines in{' '}
            <Link to="/journal-entries" className="text-brand-600 hover:underline">Journal Entries</Link>.
          </li>
          <li>
            <strong>Restock via purchase</strong> — create a{' '}
            <Link to="/purchase-orders/new" className="text-brand-600 hover:underline">purchase order</Link>,
            confirm, and receive into a warehouse. AP journal entry posts and settles.
          </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>
  )
}