Daniel Nguyen
← All work

Money-safe finance data entry and spreadsheet exports

A spreadsheet-like finance screen with exact BigInt totals, blank kept apart from zero, visible save conflicts, and exports that stop rather than write partial data.

TTMI
Exact totalsNo fabricated zeros, no partial exports
Area
Retail ERP
When
2025–2026
My role
Frontend developer
  • TypeScript
  • React
  • TanStack Query
  • TanStack Table
  • ExcelJS
  • SheetJS
  • Vitest

How it fits together

Money-safe finance data entry and spreadsheet exportsEntry grid saves with a base revision and keeps its draft on conflict; exports fetch in batches and write a file only when every batch succeeds.Finance userpersonEntry grid + draftserviceMoney utilitiescheck or alarmExport builderworkerAPI serviceserviceRelational DBdata storeExcel workbookexternaltype or paste valuesBigInt sums, blank ≠ 0save with base revisionwrite if revision freshconflict: keep draftexport requestbounded batch fetchesall batches ok: write
Entry grid saves with a base revision and keeps its draft on conflict; exports fetch in batches and write a file only when every batch succeeds.
  • Person
  • Service
  • Check or alarm
  • Worker
  • Data store
  • External

The problem

TTMI’s internal ERP is used across 50 stores, ~500 employees and 4 brands. Finance staff enter daily sales figures in a grid that behaves like a spreadsheet: they paste blocks, drag-select and move around with the keyboard. I built the frontend for this area and worked with the backend on the API contract.

Four things could go wrong.

Totals had to be exact. JavaScript numbers are floating point, so they cannot represent many amounts exactly and they lose precision on very large sums.

An empty cell had to mean “not entered yet”. If the UI treated it as zero, dashboards would show revenue figures that nobody had typed in.

Several people could edit the same period. A browser tab opened an hour ago must not silently overwrite newer data.

Exports to Excel had their own issues. One formatting rule applied to whole columns made whole-number amounts show a dangling decimal separator and risked turning identifiers such as staff codes into numbers. A company-wide monthly export also needed per-employee detail, and one request per employee was slow.

What I did

Integer money, summed with BigInt

Amounts travel and live in the client as integer strings, and totals are summed with BigInt. Thousands separators exist only on screen. This keeps totals exact well beyond the range where floating point breaks. The trade-off is conversion code at every boundary: input, paste, display and payload.

Blank is its own state

“Not entered” stays distinct from zero through display, paste, the request payload and what the API stores. Dashboards show entered days against missing days, so a gap never looks like a slow day. Every aggregate needed a separate path for blanks, and the product owners had to agree on what an empty cell means before I wrote any arithmetic.

Revision checks with a visible conflict

Each save sends the revision it was based on, and the API rejects stale writes. On the client, the save shows a before-and-after total for confirmation, blocks double submits, and keeps the local draft if the save fails or conflicts. Users occasionally have to reload and re-apply their changes. I chose that over last-write-wins, where one person’s figures vanish without anyone noticing.

One number format helper for every export

I wrote a shared export helper that picks the cell format from the actual value, whole or fractional, and applies it only to measurable fields so identifiers stay as text. Every export module now routes through it. It is less declarative than configuring each column, and it only works if every exporter uses it, which I enforced by moving the existing exports onto it.

Batched exports that are all or nothing

For the company-wide export, I replaced per-employee requests with compact batch requests fetched in small parallel waves. If any batch fails, the export stops and shows an error. A single failing batch blocks the whole file. I accepted that because a missing file is safer than a payroll sheet with zeros where data failed to load.

Result

The finance screens and exports are in production and used by the finance team. Totals are exact, blanks are never counted as zero, and two people editing the same period get a visible conflict.

I verified exported workbooks by reading them back programmatically and comparing them with the on-screen grid, with Vitest covering the money and export helpers. One formatting rule now governs every export, and the company-wide export needs a handful of batch round trips. I did not measure its timing, so I make no speed claim.

What I’d do differently

I would settle the meaning of an empty cell with finance on day one, before any grid code existed, because changing it later touched every aggregate. I would also time the batched export before and after, so its improvement could be stated with a number.