LTT Engine Room · Internal

KhuanLoke Claims Build Spec

Hand-off spec for the KhuanLoke claims app.
v1
26-Jun-26
KhuanLokeClaims_BuildSpecHandoff.md
LTT Outsourced CFO Sdn. Bhd.

KhuanLoke Claims — Build Spec & Handoff (LTT internal standard)

Purpose: everything needed to recreate this expense-claim app for another client, from scratch, in a new chat. Product name shown in app: " Claims" · tagline "From paper receipt to posted expense." Prepared by: LTT Outsourced CFO Sdn. Bhd. · reference build = KhuanLoke Claims v13 (single self-contained HTML + optional PWA set).

This supersedes the earlier PrimaHQ handoff. Follow it exactly to keep LTT apps consistent.


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 + spot-checked expense line items. 2. Re-classify them (account name + account code + type), exclude non-claim items. 3. Analyse on a dashboard (KPIs + interactive charts with drill-down). 4. Brand and export per-month expense-claim reports (client logo + details) with the scanned receipts appended. 5. Export a full-year accounting-import file (Bukku) and a Google-Sheets master. 6. Ship as an installable PWA hosted on ltt-cfo.my (offline-capable, home-screen installable).

Tech: plain HTML/CSS/JS, no framework. CDN libs: SheetJS (xlsx), jsPDF + jspdf-autotable (PDF), Chart.js 4 (dashboard charts). Poppins + KaiTi/Noto Serif SC (Chinese). Data persists in localStorage (key bumped every version, e.g. khuanloke_v13). Receipts ship as a separate JSON pack that auto-loads from the same hosting folder.


2. Inputs to collect from the new client (before building)

  1. The LERBinderReaper extraction (the human-reviewed OCR source of truth) — a folder _BR-<yymmdd>.<hhmmss>/ containing:
  2. LERBinderReaper-v<NN>-expenses-<stamp>.xlsx — the 44-column Expenses sheet + Summary by account, Exceptions, Column Guide, etc.
  3. The renamed receipt PDFs, sorted into folders: Financial/, Correspondence/, BankStatements/, NameCards/, Legal/, Statements/, Unsorted/, Utilities/, etc. Each PDF is a single-page extract of one receipt, named house-style.
  4. Client branding — company legal name, registration/SSM no., TIN, full address, tel/email, and a logo (PNG/JPG). The report accent colour ("spine") is auto-derived from the logo.

The client will re-issue the extraction several times as they fix OCR/classification errors. Each time: clear the old data and rebuild from the newest _BR-... folder. Always confirm which folder is current.


3. Data model

Each expense line ("row") object:

{ id, client, clientName, date (YYYY-MM-DD), month (YYYY-MM),
  supplier, desc, category (= account name), ref, amount (number, RM),
  note, file (relative path "<Folder>/<OutputFile>"), pages [ints], type, inc }

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)} }. Single-entity apps show one client and hide the entity switcher.


4. Mapping the LERBinderReaper xlsx → dataset (CRITICAL — do this consistently)

Parse the Expenses sheet (skip the header row; skip any row where No is blank — those are blank/grand-total artifacts). For each row with an Output File:

Seed categories = distinct {name, code} from the rows. Set excluded:true for Unclassified (bank statements, name cards, legal docs — not claims). All real IE accounts excluded:false.

Known per-client corrections (re-apply each rebuild if the extraction still has them): e.g. a food bill mis-tagged to a travel account → reclassify in the parser (KhuanLoke: "MK Porridge" Toll→Food).

Sanity: after parsing, print row count, total RM, per-account totals, and confirm every row's category exists in seed categories and every file exists on disk.


5. Classification rules (confirmed house style)


6. App structure (left sidebar + finance theme)

Left sidebar (256px, fixed): brand (client logo + " Claims 群乐报销") + grouped nav (active = green-tint pill + gold left bar) + "Designed by LTT Outsourced CFO" footer with the version chip. On mobile (≤820px) the sidebar becomes a horizontal scrollable top nav.

Content (shifted right): a slim top bar showing entity name + registration, then the tab panels.

Tabs: 1. Dashboard — a green gradient hero banner (title + description + status pill), 4 KPI cards with icon chips (Claimable filled green; Excluded/Accounts gold accents; cards are clickable → their tab), then 4 interactive Chart.js charts (Claimable by month, by account, Included vs excluded, Top-10 suppliers) with rounded data labels, then by-account table and a red "accounts without a code" list. All charts + the account table drill down on click → a modal ledger (Date · Supplier · Description · Account · Ref · Amount · Receipt-👁), with sortable headers and receipt preview. 2. Expenses — claimable items, editable inline (auto-growing supplier/description fields, category/type dropdowns, In/Out). Sortable headers, page-count + 👁 preview. Filters: month/category/type/search. Renders capped at 150 rows with a "show all" button (prevents freeze on ~1,700 rows). 3. Excluded — excluded items grouped by reason, muted text + "excl" tag (no stripes/strike-through). 4. Export — full-year Bukku (.xlsx); per-month branded report with receipts appendix: Export monthly PDFs (one file per month), Export .pdf (combined), Print preview, Download .html. Filters: Period (dropdown, hides future months, defaults to current month), Account/category, Vendor/merchant (assisted-fill datalist of real supplier names). All exports capped at the current calendar month. Receipts auto-load from the same folder. 5. Settings — client details + logo + accent colour; categories (name, code, Bukku name, Excluded).


7. Report & export formats


8. Visual identity — LTT finance theme (use these exact tokens)

--green:#0A4D31; --green-dark:#063824; --green-tint:#EAF2EC; --green-mid:#138A5A; --green-soft:#F3F8F4;
--gold:#B68A2E; --gold-dark:#8E6B1C; --gold-tint:#FAF3E3;
--red:#BC2A2A; --red-tint:#FBECEC;
--ink:#16201A; --graphite:#37433C; --soft:#6E776F; --faint:#9AA39B;
--bg:#F4F6F2; --card:#FFFFFF; --line:#E6E9E2; --line-2:#D7DBD2; --sb:256px;

Font: Poppins + KaiTi/Noto Serif SC (Chinese). Table headers = light green-soft with soft-grey uppercase text. Cards white, rounded 12–14px. App chrome uses the client logo (top bar / sidebar brand); reports use the client logo + LTT "prepared by" footer. Chart palette = soft pastels; data labels rounded to 0 decimals (Top-10 suppliers no decimals). Client report accent = the client logo's dominant colour ("spine").


9. Build pipeline

The app is one HTML file. Data is injected by replacing three single-line constants: - const DATASET=[...] — the parsed rows (Section 4). - const SEED_CATS=[...] — categories {name,code,bukku,excluded}. - const CLIENT_DEFAULTS={...} — {code:{name,reg,tin,addr,tel,email,logo(base64),spine}}.

Fastest path when the code hasn't changed: take the previous finished app HTML and swap only the DATASET + SEED_CATS lines, then bump the localStorage key + version chip. Do NOT re-run the whole feature chain for a data-only refresh.

Logo: trim transparent border, resize to ~560px wide, base64 → data URI; auto-derive spine = dominant non-neutral colour.

Receipts pack (separate JSON, ~27 MB): render each renamed PDF with pdftoppm -jpeg -r 76, resize ≤820px wide, JPEG q34, base64. Key = file + "|" + page. Because the extracts are single-page but the dataset's pages reference the original scan page, render the file's ACTUAL page and map every requested file|page key to it (fallback to page 1). Generate resumably in ~38s batches. Verify every dataset lookup (file|page) resolves.

Assembly / validation: write big files via bash heredoc (host file tools truncate large writes); node --check the extracted inline <script> before shipping; verify all row categories ∈ seed cats, dates ISO, receipts resolve.


10. PWA packaging (for ltt-cfo.my hosting)

Folder <Client>Claims-PWA-v<N>-<yymmdd>/ containing: - index.html — the app + <link rel="manifest">, theme-color #0A4D31, apple/mobile meta, and a service-worker registration script. - manifest.webmanifest — name, standalone display, #0A4D31 theme, white background, relative start_url/scope, icons. - service-worker.js — precache shell + <Client>Claims-Receipts.json; runtime-cache everything else (CDN libs) → full offline after first visit. Bump the CACHE name every version (<client>-claims-v<N>). - KhuanLokeClaims-Receipts.json — the receipts pack (auto-fetched by the app on load). - icons/ — 192, 512, apple-180, maskable-512 (green bg) generated from the logo. Upload the folder over HTTPS; the app auto-loads receipts from the same folder, is installable, and works offline. The standalone single HTML is also delivered (with the receipts JSON beside it) for offline double-click use.


11. Naming & versioning conventions


12. Known constraints / gotchas


13. Step-by-step recipe for a NEW client

  1. Get the newest _BR-<stamp>/ extraction folder + logo + company details. Confirm it's the current one.
  2. Parse the Expenses xlsx → DATASET + SEED_CATS (Section 4): RM After-Check amounts, house-style suppliers, Unclassified excluded, per-client fixes, notes for flagged/foreign.
  3. Embed the logo (trim/base64) + set CLIENT_DEFAULTS + spine.
  4. Swap DATASET/SEED_CATS/CLIENT_DEFAULTS into the reference app HTML; rebrand " Claims"; bump version + localStorage key; node --check.
  5. Generate the receipts pack from the PDFs (keyed file|page, single-page handling); verify all lookups resolve.
  6. Package the PWA set (Section 10) + the standalone; upload the PWA folder to ltt-cfo.my over HTTPS.
  7. On every re-issue of the data: clear old data, rebuild from the newest folder, increment the version, delete the prior version.

14. Current asset inventory (KhuanLoke, this project)

ler.lttcfo.ai/appclaim