feat(d22): P2 — onboarding checklist per project + Projects (Onboarding) board
Per client-module in the Modules tab: a MilestoneChecklist (tick with date, + Add step), reloading in place. New Onboarding work page: Stats header + per-milestone board (done/total, pending badge, warn/ok tint) with click-through to the pending projects for each milestone (client links to Client 360). Nav + /projects route. typecheck + web build clean, web tests 4/4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
3c2e51ae6c
commit
c5480cf8cd
@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
Badge, Button, DataTable, EmptyState, ErrorState, PageHeader, Skeleton, StatCard, Stats, Toolbar,
|
||||||
|
} from '@sims/ui'
|
||||||
|
import { getPendingProjects, getProjectBoard, type MilestoneBoardRow, type PendingProject } from '../api'
|
||||||
|
import { useData } from './Clients'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onboarding board (D22): per-milestone completion across every active project. Each
|
||||||
|
* row is one checklist step with its done/total and a pending-count badge — green when
|
||||||
|
* the whole book has cleared it, amber while anyone is still behind. Click a row (or
|
||||||
|
* "View pending") to drill into the projects still waiting on that step, each linking
|
||||||
|
* back to its client 360°. Per-project ticking lives on the client's Modules tab.
|
||||||
|
*/
|
||||||
|
export function Projects() {
|
||||||
|
const board = useData(getProjectBoard, [])
|
||||||
|
const [selected, setSelected] = useState<MilestoneBoardRow | undefined>()
|
||||||
|
const pending = useData<PendingProject[]>(
|
||||||
|
() => (selected !== undefined ? getPendingProjects(selected.key) : Promise.resolve([])),
|
||||||
|
[selected?.key],
|
||||||
|
)
|
||||||
|
|
||||||
|
const rows = board.data
|
||||||
|
const cleared = rows?.filter((r) => r.pending === 0 && r.total > 0).length ?? 0
|
||||||
|
const attention = rows?.filter((r) => r.pending > 0).length ?? 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wf-page">
|
||||||
|
<PageHeader
|
||||||
|
title="Onboarding"
|
||||||
|
desc="Every onboarding step across all active projects — how many have cleared it and who is still pending. Click a step to see the stragglers."
|
||||||
|
/>
|
||||||
|
<Stats>
|
||||||
|
<StatCard label="Steps tracked" value={rows !== undefined ? String(rows.length) : '…'} />
|
||||||
|
<StatCard label="Fully cleared" value={rows !== undefined ? String(cleared) : '…'} hint="every project done" hintTone="ok" />
|
||||||
|
<StatCard label="Needs attention" value={rows !== undefined ? String(attention) : '…'} hint="someone still pending" hintTone={attention > 0 ? 'warn' : 'ok'} />
|
||||||
|
</Stats>
|
||||||
|
|
||||||
|
{board.error !== undefined && <ErrorState message={board.error} onRetry={board.reload} />}
|
||||||
|
{board.error !== undefined ? null
|
||||||
|
: rows === undefined ? <Skeleton rows={6} />
|
||||||
|
: rows.length === 0 ? <EmptyState>No active projects yet — assign a module to a client to start tracking onboarding.</EmptyState> : (
|
||||||
|
<DataTable
|
||||||
|
columns={[
|
||||||
|
{ key: 'step', label: 'Step' },
|
||||||
|
{ key: 'done', label: 'Done', numeric: true },
|
||||||
|
{ key: 'pending', label: 'Pending', numeric: true },
|
||||||
|
{ key: 'act', label: '' },
|
||||||
|
]}
|
||||||
|
rowTone={(i) => (rows[i]!.pending > 0 ? 'warn' : 'ok')}
|
||||||
|
onRowClick={(_row, i) => setSelected(rows[i]!)}
|
||||||
|
rows={rows.map((r) => ({
|
||||||
|
step: r.label,
|
||||||
|
done: `${r.done} / ${r.total}`,
|
||||||
|
pending: r.pending > 0 ? <Badge tone="warn">{r.pending}</Badge> : <Badge tone="ok">0</Badge>,
|
||||||
|
act: (
|
||||||
|
// The <tr> selects on click; stop the button from double-firing.
|
||||||
|
<span onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Button onClick={() => setSelected(r)}>View pending</Button>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selected !== undefined && (
|
||||||
|
<>
|
||||||
|
<Toolbar>
|
||||||
|
<h3 style={{ margin: 0 }}>Pending: {selected.label}</h3>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
{pending.data !== undefined && <Badge tone={pending.data.length > 0 ? 'warn' : 'ok'}>{pending.data.length} pending</Badge>}
|
||||||
|
<Button onClick={() => setSelected(undefined)}>Close</Button>
|
||||||
|
</Toolbar>
|
||||||
|
{pending.error !== undefined && <ErrorState message={pending.error} onRetry={pending.reload} />}
|
||||||
|
{pending.error !== undefined ? null
|
||||||
|
: pending.data === undefined ? <Skeleton rows={4} />
|
||||||
|
: pending.data.length === 0 ? <EmptyState>Every active project has cleared “{selected.label}”.</EmptyState> : (
|
||||||
|
<DataTable
|
||||||
|
columns={[
|
||||||
|
{ key: 'client', label: 'Client' },
|
||||||
|
{ key: 'module', label: 'Module' },
|
||||||
|
]}
|
||||||
|
rows={pending.data.map((p) => ({
|
||||||
|
client: <Link to={`/clients/${p.clientId}`}>{p.clientName}</Link>,
|
||||||
|
module: <><span className="mono">{p.moduleCode}</span> · {p.moduleName}</>,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue