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: "
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)
- The LERBinderReaper extraction (the human-reviewed OCR source of truth) — a folder
_BR-<yymmdd>.<hhmmss>/containing: LERBinderReaper-v<NN>-expenses-<stamp>.xlsx— the 44-columnExpensessheet +Summary by account,Exceptions,Column Guide, etc.- 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. - 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 }
- type ∈
Cash Bill|Bill (Credit Purchase)|Purchase Payment|Bank Money Out(mapped from DocType). - inc override:
""=auto (follow category's excluded flag),"in"=force include,"out"=force exclude. - category is the account NAME; each seed category also carries
code(account code) + optionalbukkuname +excludedflag:{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)} }. 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:
- id =
<Client>-NNNNfromNo(zero-padded 4). - date =
Date(first 10 chars); month = date[:7]. - supplier (house style) =
Brand+ (ifLegalName≠Brand →(LegalName)) + (ifOutlet→.Outlet). e.g.99 Speed Mart (99 Speedmart Sdn. Bhd.).Damai Perdana. - desc =
Itemised. - category =
Account Name(else"Unclassified"); code captured fromAccount Code. - ref =
Ref; type fromDocType. - amount (RM) =
RM After-Check(the spot-checked value — ALWAYS prefer this) → fallbackAmount(RM)→Amount→RM Equiv. Never sumAmountraw (OCR errors/foreign currency inflate it). - file =
<Category>/<Output File>; pages = integers parsed fromSrc Page/Region(e.g. "p3" → [3]). - note = if
Flag≠None →"Flagged <Flag> (<Suggest Basis>) — verify"; if foreign currency → append"Orig <CUR> <Amount>".
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)
- True-date placement: each item sits in the month of its real document date.
- Amounts: use the spot-checked RM (Section 4). Flagged/foreign rows carry a "verify" note for in-app review.
- Supplier: brand/trading name first, legal name in parentheses (keep "Sdn. Bhd."), branch after a dot; keep Chinese characters.
- Exclusions: Rental, Utilities, Payroll, bank-paid, duplicates, and the Unclassified bucket are kept out of the claim.
6. App structure (left sidebar + finance theme)
Left sidebar (256px, fixed): brand (client logo + "
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
- Per-month claim report (SparkReceipt-style): header (client logo + legal name + reg + TIN + address; logo sized so it never overlaps the title/address) → Reimburse amount + period → Expense Summary grouped by account with subtotals (
# · Date · Name · Categorisation · Amount) → Total → Summary by category (Account code | Account name | No. of bills | Amount) → Approved/Reimbursed signatures → then each numbered claim's appendix block (fields stacked vertically & wrapped so long "Brand.Outlet" names never collide) followed by its scanned receipt(s). Theme colour = clientspine. Months run newest-first; future months excluded. - Bukku export (.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:
ID, Client, Date, Month, Supplier, Description, Category, Type, Reference, Amount, Note, InReport+ Categories sheet. Import matches by ID. - Spelling: UK/Malaysia English — "Categorisation", "Itemised" (s not z).
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
- Standalone app:
<Client>Claims-v<N>-<yymmdd>.html(dropped the oldLTHExpIQ_EntityName(...)scheme). - PWA folder:
<Client>Claims-PWA-v<N>-<yymmdd>/. - Receipts pack:
<Client>Claims-Receipts.json. - Increment the version (v1→v2→…) on every delivered update — filename, the visible version chip in the sidebar footer, the
localStoragekey, and the SW cache name all move together. Keep only the latest version in the folder (delete the prior one) unless asked to retain history. - Bump the
localStoragekey whenever the embedded DATASET changes so users load fresh (not stale cached state).
12. Known constraints / gotchas
- Large file writes truncate via host file tools — write the template/generators/final HTML via bash heredoc on the Linux side;
node --checkthe script. - Rendering ~1,700 rows freezes the Expenses tab — cap to 150 rows + "show all", and batch textarea auto-sizing (one read/write pass, not per-element reflow).
- Receipts pack size ~25–30 MB for a full year at 76 DPI / q34 / ≤820px; build in batches (~1,500 files/run fits the time limit).
- CDN dependency: fonts/SheetJS/jsPDF/Chart.js load from cdnjs; needs internet on first load (the SW then caches them). No offline render tool exists here — always ask the client to eyeball the result in a real browser.
- Future-dated OCR rows appear from date misreads; the dashboard shows them (to spot/fix) but all exports are capped at the current month.
filterfuture / current-month usesnew Date().toISOString().slice(0,7)string compare againstYYYY-MM.
13. Step-by-step recipe for a NEW client
- Get the newest
_BR-<stamp>/extraction folder + logo + company details. Confirm it's the current one. - Parse the
Expensesxlsx →DATASET+SEED_CATS(Section 4): RM After-Check amounts, house-style suppliers, Unclassified excluded, per-client fixes, notes for flagged/foreign. - Embed the logo (trim/base64) + set
CLIENT_DEFAULTS+ spine. - Swap
DATASET/SEED_CATS/CLIENT_DEFAULTSinto the reference app HTML; rebrand "Claims"; bump version + localStoragekey;node --check. - Generate the receipts pack from the PDFs (keyed
file|page, single-page handling); verify all lookups resolve. - Package the PWA set (Section 10) + the standalone; upload the PWA folder to ltt-cfo.my over HTTPS.
- 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)
- App:
KhuanLokeClaims-v13-260626.html(+KhuanLokeClaims-Receipts.jsonbeside it). - PWA set:
KhuanLokeClaims-PWA-v13-260626/(index.html, manifest.webmanifest, service-worker.js [cachekhuanloke-claims-v13], icons/, receipts json). - Client: Tarian Singa Khuan Loke Sdn. Bhd., 202401053054 (1598896-W), TIN C59751011030, Plaza Seri Setia, Petaling Jaya. Single entity
TSKhuanLoke. Spine#004818(from logo). - Data source:
Scan-260626-Final/_BR-260626.031326/(LERBinderReaper v75). 1,739 rows, total ≈ RM283,484 (spot-checked); 11 IE accounts + excluded Unclassified.