diff --git a/apps/hq/src/repos-milestones.ts b/apps/hq/src/repos-milestones.ts index 5aba315..b2f2433 100644 --- a/apps/hq/src/repos-milestones.ts +++ b/apps/hq/src/repos-milestones.ts @@ -12,9 +12,9 @@ import type { DB } from './db' export interface Milestone { key: string; label: string; done: boolean; doneOn: string | null; sort: number } interface MilestoneRow { key: string; label: string; done: number; done_on: string | null; sort: number } -interface TemplateEntry { key: string; label: string } +export interface TemplateEntry { key: string; label: string } -const FALLBACK_TEMPLATE: TemplateEntry[] = [ +export const FALLBACK_TEMPLATE: TemplateEntry[] = [ { key: 'advance_paid', label: 'Advance payment received' }, { key: 'setup_done', label: 'Setup / configuration done' }, { key: 'installed', label: 'Installation done' }, @@ -23,14 +23,23 @@ const FALLBACK_TEMPLATE: TemplateEntry[] = [ { key: 'amc_start', label: 'AMC / renewal start' }, ] -/** The standard checklist (owner-editable via the setting; fallback to the code default). */ -export async function milestoneTemplate(db: DB): Promise { +/** The checklist for a module (owner-editable per module via setting; global default; code fallback). */ +export async function milestoneTemplate(db: DB, moduleCode?: string): Promise { + const parse = (value: string): TemplateEntry[] | undefined => { + try { + const p = JSON.parse(value) as TemplateEntry[] + return Array.isArray(p) && p.length > 0 ? p : undefined + } catch { return undefined } + } + if (moduleCode !== undefined && moduleCode !== '') { + const perModule = await db.get<{ value: string }>( + `SELECT value FROM setting WHERE key=?`, `project.milestone_template:${moduleCode}`) + const parsed = perModule !== undefined ? parse(perModule.value) : undefined + if (parsed !== undefined) return parsed + } const row = await db.get<{ value: string }>(`SELECT value FROM setting WHERE key='project.milestone_template'`) - if (row === undefined) return FALLBACK_TEMPLATE - try { - const parsed = JSON.parse(row.value) as TemplateEntry[] - return Array.isArray(parsed) && parsed.length > 0 ? parsed : FALLBACK_TEMPLATE - } catch { return FALLBACK_TEMPLATE } + const parsedGlobal = row !== undefined ? parse(row.value) : undefined + return parsedGlobal ?? FALLBACK_TEMPLATE } /** @@ -39,7 +48,10 @@ export async function milestoneTemplate(db: DB): Promise { * project's own custom milestones and any ticked state are never touched. */ export async function ensureProjectMilestones(db: DB, clientModuleId: string): Promise { - const template = await milestoneTemplate(db) + const cm = await db.get<{ module_code: string }>( + `SELECT m.code AS module_code FROM client_module cm JOIN module m ON m.id = cm.module_id WHERE cm.id=?`, + clientModuleId) + const template = await milestoneTemplate(db, cm?.module_code) const existing = new Set( (await db.all<{ key: string }>(`SELECT key FROM project_milestone WHERE client_module_id=?`, clientModuleId)) .map((r) => r.key), diff --git a/apps/hq/test/milestones-templates.test.ts b/apps/hq/test/milestones-templates.test.ts new file mode 100644 index 0000000..eaaac0b --- /dev/null +++ b/apps/hq/test/milestones-templates.test.ts @@ -0,0 +1,30 @@ +// apps/hq/test/milestones-templates.test.ts — per-module onboarding template resolution. +import { describe, it, expect, beforeEach } from 'vitest' +import { openDb, type DB } from '../src/db' +import { milestoneTemplate } from '../src/repos-milestones' + +describe('per-module milestone templates', () => { + let db: DB + beforeEach(() => { db = openDb(':memory:') }) + + it('falls back to the global template when no module-specific one exists', async () => { + await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`, + JSON.stringify([{ key: 'a', label: 'A' }])) + expect(await milestoneTemplate(db, 'SMS')).toEqual([{ key: 'a', label: 'A' }]) + }) + + it('prefers the module-specific template over the global one', async () => { + await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template', ?)`, + JSON.stringify([{ key: 'a', label: 'A' }])) + await db.run(`INSERT INTO setting (key, value) VALUES ('project.milestone_template:SMS', ?)`, + JSON.stringify([{ key: 'doc', label: 'Document collection' }])) + expect(await milestoneTemplate(db, 'SMS')).toEqual([{ key: 'doc', label: 'Document collection' }]) + // A different module still gets the global one. + expect(await milestoneTemplate(db, 'RTGS')).toEqual([{ key: 'a', label: 'A' }]) + }) + + it('falls back to the code default when nothing is seeded', async () => { + const t = await milestoneTemplate(db, 'SMS') + expect(t.map((e) => e.key)).toContain('go_live') + }) +})