From 10b849a9908cd09c301cf6c3b80196db3ef80b68 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Sat, 18 Jul 2026 01:55:48 +0530 Subject: [PATCH] feat(d25): module display order (sort_order) + username login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - module.sort_order (both engines, migrate + pg 008); listModules orders by sort_order then code; createModule/updateModule + ModulePatch carry it, so the app-wide module order is owner-editable config (default 100 = trails). - Login accepts a username: field is type=text (autocomplete username), relabeled 'Email or username' — a plain 'admin' login now works. Module + web tests green. Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/Login.tsx | 4 ++-- apps/hq/src/db.ts | 8 +++++++- apps/hq/src/migrations-pg.ts | 4 ++++ apps/hq/src/repos-modules.ts | 17 +++++++++++------ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/hq-web/src/Login.tsx b/apps/hq-web/src/Login.tsx index b301f5b..8574faa 100644 --- a/apps/hq-web/src/Login.tsx +++ b/apps/hq-web/src/Login.tsx @@ -40,9 +40,9 @@ export function Login() { {/* A real
so Enter submits from either field (not just password). */} { e.preventDefault(); void submit() }}>

Sign in

- + setEmail(e.target.value)} /> diff --git a/apps/hq/src/db.ts b/apps/hq/src/db.ts index 7039bd2..6b7710b 100644 --- a/apps/hq/src/db.ts +++ b/apps/hq/src/db.ts @@ -174,7 +174,10 @@ CREATE TABLE IF NOT EXISTS module ( -- FieldDef[] { key, label, type, options?, required?, help?, secret?, sort? }. -- The per-client values live on client_module (field_values + secrets_enc), -- validated + rendered against this — a new module/field needs no code change. - field_spec TEXT NOT NULL DEFAULT '[]' + field_spec TEXT NOT NULL DEFAULT '[]', + -- D25: fixed display order across the app (config over code — owner-editable). + -- Lower sorts first; ties broken by code. Default 100 so unseeded modules trail. + sort_order INTEGER NOT NULL DEFAULT 100 ); CREATE TABLE IF NOT EXISTS module_price_book ( id TEXT PRIMARY KEY, module_id TEXT NOT NULL, edition TEXT NOT NULL DEFAULT 'standard', @@ -440,6 +443,9 @@ function migrate(db: SqliteRaw): void { if (!modCols2.some((c) => c.name === 'field_spec')) { db.exec(`ALTER TABLE module ADD COLUMN field_spec TEXT NOT NULL DEFAULT '[]'`) } + if (!modCols2.some((c) => c.name === 'sort_order')) { + db.exec(`ALTER TABLE module ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 100`) + } rebuildStaffUserRoleCheck(db) rebuildReminderRuleKindCheck(db) } diff --git a/apps/hq/src/migrations-pg.ts b/apps/hq/src/migrations-pg.ts index 1e825b2..7c71b45 100644 --- a/apps/hq/src/migrations-pg.ts +++ b/apps/hq/src/migrations-pg.ts @@ -281,4 +281,8 @@ CREATE INDEX IF NOT EXISTS ix_audit_entity ON audit_log (entity, entity_id); CREATE INDEX IF NOT EXISTS ix_milestone_cm ON project_milestone (client_module_id); `, }, + { + id: '008-module-sort-order', + sql: `ALTER TABLE module ADD COLUMN IF NOT EXISTS sort_order integer NOT NULL DEFAULT 100;`, + }, ] diff --git a/apps/hq/src/repos-modules.ts b/apps/hq/src/repos-modules.ts index f4894a6..a5cb163 100644 --- a/apps/hq/src/repos-modules.ts +++ b/apps/hq/src/repos-modules.ts @@ -57,12 +57,13 @@ export interface Module { allowedKinds: Kind[]; multiSubscription: boolean; active: boolean quoteContent: string[] // optional "what's included" lines that flow onto quotes fieldSpec: FieldDef[] // D21: the operational fields this module declares + sortOrder: number // D25: fixed display order (lower first); owner-editable } interface ModuleRow { id: string; code: string; name: string; sac: string allowed_kinds: string; multi_subscription: number; active: number - quote_content: string; field_spec: string + quote_content: string; field_spec: string; sort_order: number } function toModule(r: ModuleRow): Module { @@ -73,13 +74,14 @@ function toModule(r: ModuleRow): Module { active: r.active === 1, quoteContent: JSON.parse(r.quote_content) as string[], fieldSpec: JSON.parse(r.field_spec ?? '[]') as FieldDef[], + sortOrder: r.sort_order ?? 100, } } export interface ModuleInput { code: string; name: string; sac?: string allowedKinds?: Kind[]; multiSubscription?: boolean; quoteContent?: string[] - fieldSpec?: FieldDef[] + fieldSpec?: FieldDef[]; sortOrder?: number } export async function getModule(db: DB, id: string): Promise { @@ -88,7 +90,8 @@ export async function getModule(db: DB, id: string): Promise { } export async function listModules(db: DB): Promise { - const rows = await db.all(`SELECT * FROM module ORDER BY code`) + // D25: fixed business display order (sort_order), code as the stable tiebreak. + const rows = await db.all(`SELECT * FROM module ORDER BY sort_order, code`) return rows.map(toModule) } @@ -100,11 +103,12 @@ export async function createModule(db: DB, userId: string, input: ModuleInput): const id = uuidv7() const fieldSpec = input.fieldSpec !== undefined ? validateFieldSpec(input.fieldSpec) : [] await db.run( - `INSERT INTO module (id, code, name, sac, allowed_kinds, multi_subscription, quote_content, field_spec) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO module (id, code, name, sac, allowed_kinds, multi_subscription, quote_content, field_spec, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, input.code, input.name, input.sac ?? '998313', JSON.stringify(kinds), input.multiSubscription === true ? 1 : 0, JSON.stringify(input.quoteContent ?? []), JSON.stringify(fieldSpec), + input.sortOrder ?? 100, ) const mod = (await getModule(db, id))! await writeAudit(db, userId, 'create', 'module', id, undefined, mod) @@ -114,7 +118,7 @@ export async function createModule(db: DB, userId: string, input: ModuleInput): export interface ModulePatch { name?: string; sac?: string; allowedKinds?: Kind[] multiSubscription?: boolean; active?: boolean; quoteContent?: string[] - fieldSpec?: FieldDef[] + fieldSpec?: FieldDef[]; sortOrder?: number } export async function updateModule(db: DB, userId: string, id: string, patch: ModulePatch): Promise { @@ -134,6 +138,7 @@ export async function updateModule(db: DB, userId: string, id: string, patch: Mo if (patch.active !== undefined) { sets.push('active=?'); args.push(patch.active ? 1 : 0) } if (patch.quoteContent !== undefined) { sets.push('quote_content=?'); args.push(JSON.stringify(patch.quoteContent)) } if (patch.fieldSpec !== undefined) { sets.push('field_spec=?'); args.push(JSON.stringify(validateFieldSpec(patch.fieldSpec))) } + if (patch.sortOrder !== undefined) { sets.push('sort_order=?'); args.push(patch.sortOrder) } if (sets.length > 0) { args.push(id) await db.run(`UPDATE module SET ${sets.join(', ')} WHERE id=?`, ...args)