PrimaHQ Claims — Build Spec & Handoff
Purpose: everything needed to recreate this expense-claim app for another client, from scratch, in a new chat. Product name shown in app: "PrimaHQ Claims" · tagline "From paper receipt to posted expense." Prepared by: LTT Outsourced CFO Sdn. Bhd. · current build = v9 (single self-contained HTML).
1. What the app is
A single self-contained .html file (double-click to run offline; no server) that lets a bookkeeper:
1. Hold a client's whole-year OCR'd expense line items.
2. Re-classify them (category + account code + type), exclude non-claim items.
3. Brand and export per-month expense-claim reports (client logo + details) with the scanned receipts appended.
4. Export a full-year accounting-import file (Bukku) and a PDF/HTML, plus a Google-Sheets master for bulk editing.
Tech: plain HTML/CSS/JS. CDN libs: SheetJS (xlsx read/write), jsPDF + jspdf-autotable (direct PDF). Poppins font + Noto/KaiTi for Chinese. Data persists in the browser via localStorage (key bumped each version, e.g. primahq_v9). No backend.
2. Inputs to collect from the new client (before building)
- Scanned receipts for the year — usually monthly PDFs/zip of receipts (each month a folder or a bundle PDF). Many are scanned images (need OCR), mixed English/Malay/Chinese + handwriting.
- A classification workbook (the human-reviewed source of truth) — an
.xlsxwith 3 tabs: Claims to submit (not via bank)— the items that go into the claim.Excluded - bank-paid & dup— already paid via bank / duplicates (kept out).Excluded - utilities & rental— handled separately (kept out). Columns used: Reference No., Date, Supplier / Payee, Description, Original Category, Acct Code, Account (PrimaHQ classification), Amount (RM), Original Ref, Status, Source File, Page(s).- Client branding — company legal name, registration/SSM no., TIN, full address, and a logo (PNG/JPG). The report theme colour is auto-derived from the logo.
If there's no reviewed workbook yet, you OCR the receipts first (Section 6) to produce a draft dataset, then the client reviews/splits it into the 3 tabs.
3. Data model
Each expense line ("row") object:
{ id, client, clientName, date (YYYY-MM-DD true doc date), month (YYYY-MM),
supplier, desc, category (= account name), ref, amount (number),
note, file (relative path), pages [ints], type, inc }
- type ∈
Cash Bill|Bill (Credit Purchase)|Purchase Payment|Bank Money Out. - inc override:
""=auto (follow category's excluded flag),"in"=force include,"out"=force exclude. - category is the PrimaHQ chart-of-accounts account name; each category also carries a code and an optional bukku account-name, held in a separate
categorieslist:{name, code, bukku, excluded}.
Exclusion logic: isExcluded(row) = inc==='out' ? true : inc==='in' ? false : category.excluded. Excluded rows never enter reports or the Bukku export.
Clients map: { code: {name, reg, tin, addr, tel, email, logo(dataURI), spine(hex)} }. spine = the report accent colour (auto-set from the logo's dominant colour).
4. Classification rules (confirmed with client — apply consistently)
- True-date placement: each item sits in the month of its real document date, not the folder it was filed in. If a client petty-cash ledger exists, the ledger's date/amount is authoritative over a stale printed receipt date.
- Partial claims: if only part of a receipt is circled/boxed/annotated as claimed, record only that marked amount.
- Utility-bill arrears: record only the current-month charge; exclude prior balances.
- Categories: use the client's PrimaHQ chart of accounts (account name + numeric code). Honour handwritten category labels.
- Supplier (house style): brand/trading name first, official legal name in parentheses (keep "Sdn. Bhd."), branch after a dot; keep Chinese characters. e.g.
99 Speed Mart (99 Speedmart Sdn. Bhd.).Damai Perdana. - Exclusions: Rental, Utilities, Payroll, bank-paid and duplicates are excluded from the claim (managed separately for bank cross-check).
5. App structure (tabs)
- Dashboard (front page): KPI cards per entity (claimable total/bills, excluded total), claimable-by-month table, claimable-by-account table, and a red "Accounts without an account code — reassign these" list.
- Expenses: claimable items only, editable inline (date, supplier, desc, category dropdown, type, ref, amount, In/Out). Sortable column headers. Page-count + 👁 receipt preview modal. Filters: month/category/type/search. Buttons: download/import master xlsx, "Build report (filtered month)".
- Excluded: excluded items, grouped by reason — Paid via bank / duplicate and Utilities & rental — each with subtotal. Export listing / report.
- Export: full-year Bukku workbook; per-month branded reports — browser print, Download .html, Export .pdf (no print dialog, via jsPDF); load receipts pack; save/load project JSON.
- Settings (last): Clients (details + logo upload + accent colour: from-logo / LTT palette / custom) and Categories (account name, code, Bukku name, Excluded checkbox; add/rename/delete).
Top bar: app name + an Entity dropdown to switch between clients/entities.
6. Report & export formats
- Per-month claim report (SparkReceipt-style): header (client logo + legal name + reg + TIN + address) → status pill → Reimburse amount + period → Expense Summary table
# · Date · Name(merchant+desc+Ref) · Categorization(code · name) · Amountgrouped by account with subtotals → Total Reimbursable → Summary by category table (Account code | Account name | No. of bills | Amount) → Approved/Reimbursed signatures → then each numbered claim's detail block (#NNN, merchant, date, total, type, category, "Attached files (N)") followed by its scanned receipt(s). Status remark is NOT printed. Theme colour = clientspine. - Bukku export (full year, .xlsx): Batch Cash Bills columns —
Supplier, Reference No., Date(dd/mm/yyyy), Currency(MYR), Pay From(Petty Cash; blank for credit bills), Account(=code - name), Item Description, Amount, Tax. Excluded rows omitted. - Master xlsx (for Google Sheets): one sheet
ID, Client, Date, Month, Supplier, Description, Category, Type, Reference, Amount, Note, InReport+ a Categories sheet. Edit in Drive → Download .xlsx → "Import edited master" matches by ID.
7. How the app is assembled (build pipeline)
The HTML is built from a template (app2_template.html) with four placeholders that get string-replaced at build time:
- __DATASET__ → JSON array of all line items (all entities).
- __SEEDCATS__ → JSON array of categories {name,code,bukku,excluded}.
- __CLIENTDEFAULTS__ → JSON of clients {code:{name,reg,tin,addr,tel,email,logo,spine}} (logo embedded as a base64 data URI).
- __LTTLOGO__ → LTT app-mark data URI (top bar only; reports use client logos).
Receipts are NOT embedded in the app (too large). They're delivered as a separate receipts pack JSON: { "<file>|<page>": "data:image/jpeg;base64,..." }, loaded at runtime on the Export tab. Keys must equal each row's file + "|" + page.
Filenames (client's convention): app = LTHExpIQ_EntityName(<EntityCodes>)-v<N>-<yymmdd>.<hhmmss>.html; receipts pack = LTHExpIQ_Receipts(<EntityCode>)-v<N>-<yymmdd>.json.
8. Visual identity (LTT SOP001)
Charcoal #323232 text (never pure black), graphite #3D424A table headers, white canvas, hairline rules, generous whitespace, tabular numerals, Poppins + KaiTi. For the app chrome the LTT crimson #C62F58 is the accent; for client reports the accent = the client logo's dominant colour (auto-extracted). Logos embedded as data URIs so files stay self-contained.
9. Known constraints / gotchas (important when rebuilding here)
- Large file writes can truncate when written via the host file tools to the mounted folder. Write big files (the template, generators, the final HTML) via a bash heredoc on the Linux side, and
node --checkthe extracted<script>before shipping. - Receipts pack size: ~30–50 MB for a full year; generate page images at ~80 DPI / JPEG q38 / max width 850 px. Build the pack incrementally in batches (≈100–130 pages/run) to fit execution time limits.
- Filename mismatches between the workbook's "Source File" and the actual receipt files (curly vs straight apostrophes, dropped words, GUID placeholders) — use a fuzzy resolver (normalise apostrophes/spaces/case) plus a distinctive-token fallback (e.g. invoice number).
- PDF export depends on the jsPDF CDN — first run needs internet; full-year PDFs are large/slow.
- Bump the
localStoragekey every version so users load fresh data, not stale cached state.
10. Step-by-step recipe for a NEW client
- Collect the 3-tab classification workbook + receipts (zips/PDFs) + logo + company details.
- Parse the workbook → build the dataset (rows with id/date/supplier/desc/category/code/type/inc/file/pages). Claims tab →
inc:"in"; the two excluded tabs →inc:"out". Capture distinct accounts → seed categories (name+code). - Embed the logo (trim white, base64) and set client details + spine (auto from logo).
- Inject dataset/seedcats/clientdefaults/logo into the template → write the app
.html(via bash heredoc) →node --check. - Generate the receipts pack from the receipt source, keyed
file|page, in batches. - Hand over: the app
.html, the receipts pack.json, and a Google-Sheets master.xlsx. - To add more entities later, append to clientdefaults + dataset and rebuild.
11. Current project asset inventory (this client)
- App:
LTHExpIQ_EntityName(TDKPrimaBestari+JCPrimaEnt+TSKPrimaBestari)-v9-...html(Tadika data loaded; JC & Taska configured, JC data pending; Taska data pending). - Template (rebuild source):
app2_template.html. - Data:
app_dataset_v5.json,_seedcats.json,_client_defaults.json. - Receipts pack:
LTHExpIQ_Receipts(TDKPrimaBestari)-v2-260624.json(sourced from the HQClaims folder). - Generators:
gen_receipts_hq.py(receipts pack), report logic lives inside the template (reportHTML/exportPDF). - Reconciliation packs (per entity, data + captioned receipt PDFs):
TDKPrimaBestari_ReconciliationPack_2025.zip,JCPrimaEnt_ReconciliationPack_2025.zip.
Tadika figures (sanity): claims 498 items / RM106,139.24; excluded 68 items / RM83,503.68. JC Prima still on the original OCR classification (44 rows) pending its workbook. Taska empty pending its workbook.