feat(onboarding): stalled-project query for the dashboard/MIS tile
Adds stalledProjects(db, olderThanDays, today) to repos-milestones.ts: active projects with a pending step whose last progress (MAX done_on, or module creation if nothing ticked) is older than N days. GET /projects/stalled reads ?days= with a project.stall_days setting fallback (default 30). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/client-detail-redesign
parent
195d821bef
commit
75fb41c366
@ -0,0 +1,57 @@
|
|||||||
|
// apps/hq/test/stalled-onboarding.test.ts — Task 8: stalled-onboarding query (dashboard/MIS tile).
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { openDb } from '../src/db'
|
||||||
|
import { seedIfEmpty } from '../src/seed'
|
||||||
|
import { createClient } from '../src/repos-clients'
|
||||||
|
import { createModule, assignModule } from '../src/repos-modules'
|
||||||
|
import { setMilestone, stalledProjects } from '../src/repos-milestones'
|
||||||
|
|
||||||
|
async function world() {
|
||||||
|
const db = openDb(':memory:')
|
||||||
|
await seedIfEmpty(db)
|
||||||
|
const c = await createClient(db, 'u1', { name: 'Kothavara SCB', stateCode: '32' })
|
||||||
|
// A module code with no per-module milestone template seeded (D22 seed.ts only carries
|
||||||
|
// SMS/RTGS/MOBILEAPP overrides), so the global template applies: advance_paid, setup_done,
|
||||||
|
// installed, go_live, balance_paid, amc_start.
|
||||||
|
const m = await createModule(db, 'u1', { code: 'MISC', name: 'Misc Module' })
|
||||||
|
return { db, c, m }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('stalledProjects (D22 — dashboard/MIS tile)', () => {
|
||||||
|
it('lists a project parked past the threshold', async () => {
|
||||||
|
const { db, c, m } = await world()
|
||||||
|
const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
// Early steps done on an old date; later steps (incl. go_live) stay pending — project is
|
||||||
|
// still active (status stays 'quoted'/'installed', never 'live').
|
||||||
|
await setMilestone(db, 'u1', cm.id, 'advance_paid', true, '2026-03-01')
|
||||||
|
await setMilestone(db, 'u1', cm.id, 'setup_done', true, '2026-03-01')
|
||||||
|
// last done_on = 2026-03-01, today = 2026-07-19 → ~140 days parked.
|
||||||
|
const stalled = await stalledProjects(db, 30, '2026-07-19')
|
||||||
|
expect(stalled.map((p) => p.clientModuleId)).toContain(cm.id)
|
||||||
|
const row = stalled.find((p) => p.clientModuleId === cm.id)!
|
||||||
|
expect(row.clientName).toBe('Kothavara SCB')
|
||||||
|
expect(row.moduleCode).toBe('MISC')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excludes a recently-progressed project', async () => {
|
||||||
|
const { db, c, m } = await world()
|
||||||
|
const cm = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
await setMilestone(db, 'u1', cm.id, 'advance_paid', true, '2026-03-01')
|
||||||
|
await setMilestone(db, 'u1', cm.id, 'setup_done', true, '2026-03-01')
|
||||||
|
const stalled = await stalledProjects(db, 365, '2026-07-19')
|
||||||
|
expect(stalled).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('excludes projects with no pending steps (fully ticked) and inactive/live projects', async () => {
|
||||||
|
const { db, c, m } = await world()
|
||||||
|
// Fully ticked project: nothing pending, so it should never appear regardless of age.
|
||||||
|
const done = await assignModule(db, 'u1', { clientId: c.id, moduleId: m.id, kind: 'yearly' })
|
||||||
|
for (const key of ['advance_paid', 'setup_done', 'installed', 'go_live', 'balance_paid', 'amc_start']) {
|
||||||
|
await setMilestone(db, 'u1', done.id, key, true, '2026-01-01')
|
||||||
|
}
|
||||||
|
const stalled = await stalledProjects(db, 1, '2026-07-19')
|
||||||
|
expect(stalled.map((p) => p.clientModuleId)).not.toContain(done.id)
|
||||||
|
// 'live' status (from ticking go_live) also excludes it, which the loop above proves.
|
||||||
|
expect((await db.get<{ status: string }>(`SELECT status FROM client_module WHERE id=?`, done.id))!.status).toBe('live')
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue