From 54d6a63e8191d60c445e7c81f66596c1f5f1c611 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 10 Jul 2026 10:19:46 +0530 Subject: [PATCH] =?UTF-8?q?feat(hq):=20HQ-1.5=20=E2=80=94=20module=20quote?= =?UTF-8?q?=20content,=20pack=20pick-lists,=20bundle=20isolation=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/api.ts | 5 +- apps/hq-web/src/pages/Modules.tsx | 52 ++++++++++++++- apps/hq-web/src/pages/NewDocument.tsx | 96 ++++++++++++++++++++++----- apps/hq/src/api.ts | 17 ++++- apps/hq/src/db.ts | 12 +++- apps/hq/src/repos-documents.ts | 18 ++++- apps/hq/src/repos-modules.ts | 40 ++++++++++- apps/hq/src/templates.ts | 11 ++- apps/hq/test/documents.test.ts | 33 +++++++++ apps/hq/test/isolation.test.ts | 79 ++++++++++++++++++++++ apps/hq/test/modules.test.ts | 28 +++++++- apps/hq/test/templates.test.ts | 13 ++++ 12 files changed, 372 insertions(+), 32 deletions(-) create mode 100644 apps/hq/test/isolation.test.ts diff --git a/apps/hq-web/src/api.ts b/apps/hq-web/src/api.ts index 167fee3..d157cd6 100644 --- a/apps/hq-web/src/api.ts +++ b/apps/hq-web/src/api.ts @@ -82,6 +82,7 @@ export const KIND_LABEL: Record = { export interface Module { id: string; code: string; name: string; sac: string allowedKinds: Kind[]; multiSubscription: boolean; active: boolean + quoteContent: string[] } export interface ModulePrice { @@ -117,7 +118,7 @@ export interface Doc { docDate: string; status: DocStatus; refDocId: string | null taxablePaise: number; cgstPaise: number; sgstPaise: number; igstPaise: number roundOffPaise: number; payablePaise: number - payload: { lines: DocLine[]; terms?: string } + payload: { lines: DocLine[]; terms?: string; lineContents?: string[][] } source: string; createdBy: string; createdAt: string } @@ -161,6 +162,8 @@ export const getModules = (): Promise => apiFetch<{ modules: Module[] }>('/modules').then((r) => r.modules) export const createModule = (body: Record): Promise => apiFetch<{ module: Module }>('/modules', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.module) +export const patchModule = (id: string, body: Record): Promise => + apiFetch<{ module: Module }>(`/modules/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.module) export const getPrices = (moduleId: string): Promise => apiFetch<{ prices: ModulePrice[] }>(`/modules/${moduleId}/prices`).then((r) => r.prices) export const addPrice = (moduleId: string, body: Record): Promise => diff --git a/apps/hq-web/src/pages/Modules.tsx b/apps/hq-web/src/pages/Modules.tsx index 0d8fa7c..219e2fd 100644 --- a/apps/hq-web/src/pages/Modules.tsx +++ b/apps/hq-web/src/pages/Modules.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { formatINR, fromRupees } from '@sims/domain' import { Badge, Button, DataTable, EmptyState, Field, Notice, PageHeader, Toolbar } from '@sims/ui' import { - addPrice, createModule, getModules, getPrices, role, + addPrice, createModule, getModules, getPrices, patchModule, role, KIND_LABEL, type Kind, type Module, } from '../api' import { useData } from './Clients' @@ -10,6 +10,9 @@ import { useData } from './Clients' const ALL_KINDS: Kind[] = ['one_time', 'monthly', 'yearly', 'usage'] const today = () => new Date().toISOString().slice(0, 10) +/** Textarea ↔ string[]: one "what's included" bullet per row, blanks dropped. */ +const splitLines = (s: string): string[] => s.split('\n').map((x) => x.trim()).filter((x) => x !== '') + /** Module catalog + dated price book. Owner edits; staff read-only. */ export function Modules() { const isOwner = role() === 'owner' @@ -48,16 +51,57 @@ export function Modules() { /> )} {selected !== undefined && ( - + <> + modules.reload()} /> + + )} ) } +/** Per-module "what's included" lines — prefilled into the composer, printed under the quote line. */ +function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () => void }) { + const [text, setText] = useState(props.module.quoteContent.join('\n')) + const [saved, setSaved] = useState(false) + const [error, setError] = useState() + + const save = () => { + setError(undefined) + patchModule(props.module.id, { quoteContent: splitLines(text) }) + .then(() => { setSaved(true); props.onSaved() }) + .catch((e: Error) => setError(e.message)) + } + + return ( + <> +

Quote content — {props.module.name}

+ {props.isOwner ? ( +
+