feat(dashboard): add Onboarding stalled tile

Surfaces stalledProjects() on the Dashboard bento grid, listing active
projects parked past the stall threshold; each row jumps to the
client's Modules tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main
Thomas Joise 1 day ago
parent d6faf0d5bd
commit 83633c86ee

@ -427,6 +427,11 @@ export const bulkSetMilestone = (
body: JSON.stringify({ clientModuleIds, key, done, ...(doneOn !== undefined ? { doneOn } : {}) }),
}).then((r) => ({ updated: r.updated }))
/** Active projects parked past the stall threshold (dashboard "Onboarding stalled" tile)
* same row shape as PendingProject; server defaults the threshold from `project.stall_days`. */
export const getStalledProjects = (): Promise<PendingProject[]> =>
apiFetch<{ projects: PendingProject[] }>('/projects/stalled').then((r) => r.projects)
// ---------- ticket desk + client branches (D20 APEX parity) ----------
export type TicketStatus = 'open' | 'in_progress' | 'waiting' | 'closed' | 'dropped'

@ -2,7 +2,7 @@ import { useState, type ReactNode } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { formatINR } from '@sims/domain'
import { Badge, DataTable, EmptyState, ErrorState, Notice, Skeleton, StatCard, Stats } from '@sims/ui'
import { displayName, getDashboard, getSmsBalances, isManagerial } from '../api'
import { displayName, getDashboard, getSmsBalances, getStalledProjects, isManagerial } from '../api'
import { QueueActions, RULE_LABEL, reminderTone } from '../components/ReminderQueue'
import { useData } from './Clients'
@ -34,6 +34,7 @@ export function Dashboard() {
const [mine, setMine] = useState(false)
const dash = useData(() => getDashboard(mine), [mine])
const smsLow = useData(getSmsBalances, []) // D28: low-balance alert, seen even without opening the SMS screen
const stalled = useData(getStalledProjects, []) // parked onboarding — active projects with no recent progress
const [err, setErr] = useState('')
const v = dash.data
const name = displayName()
@ -144,6 +145,13 @@ export function Dashboard() {
))}
</Card>
<Card title="Onboarding stalled" count={stalled.data?.length ?? 0} empty="Nothing stalled." isEmpty={(stalled.data ?? []).length === 0}>
{(stalled.data ?? []).map((p) => (
<Row key={p.clientModuleId} onClick={() => nav(`/clients/${p.clientId}?tab=modules`)}
title={p.clientName} sub={p.moduleName} />
))}
</Card>
<Card title="Follow-ups today" count={v.followUpsToday.length} empty="No follow-ups due." isEmpty={v.followUpsToday.length === 0}>
{v.followUpsToday.map((f) => (
<Row key={f.id} onClick={() => nav(`/clients/${f.clientId}`)}

Loading…
Cancel
Save