A back-office console I wrote from scratch as its only author. Staff run customer accounts, contracts, invoices, support and billing from one place, open any customer's own workspace without a password, and ask an AI Document Center questions about internal documents.
78commits, sole author
~28klines TS/TSX
~30screens, lazy loaded
~1,450legacy records migrated
Apr–Oct2026
Try it
The console, with invented customers
Open a customer row, press Ctrl K inside the demo, ask the Document Center a question.
console.example · synthetic dataInteractive demo
The interactive console needs JavaScript. It shows a dashboard with KPI sparklines, a sortable customer table with a detail drawer, support charts, a command palette and a Document Center chat with cited answers.
Recreated in vanilla JavaScript for this page. The production app is React 19, TypeScript, Vite 6, Tailwind 4, Radix UI and TanStack Table.
Recording
The production UI, on a demo-safe branch
91 seconds: the time-of-day login, overview, requests, pricing, the live card preview and settings. Mock data, no real API.
Recorded from the real React app on a branch with a mock data layer and sanitised names, first shared on LinkedIn in May 2026.
Problem
Account managers, finance and support needed one place to see a customer: contract, invoices, contacts, notes, open tickets.
Customer records lived in a legacy back office. Opening a customer's own marketing panel to reproduce a problem meant asking for credentials. Support had no view of ticket volume by day, agent or customer, and answers to recurring internal questions were spread across Word documents.
My role
I designed and built the whole frontend, the Document Center retrieval service and the scripts that moved about 1,450 legacy customer records into the new CRM tables. I worked with the backend team on the endpoints the screens needed, and asked for a bulk field where the screen would otherwise have made one request per customer.
Command palette, TR/EN, light and dark themes, role-gated modules
Architecture
How a request moves
Single-page appReact 19 with lazy routes per screen, URL-addressable pages and a chunk error boundary.
API clientRoutes each path to one of two backends by longest prefix, typed errors, per-host tokens, adapters into typed models.
List cache60-second cache where calls in flight at the same moment share one request.
Backend servicesExisting services and SQL tables, extended with the endpoints the screens needed.
UploadWord documents parsed to text, split into 800-token chunks with 150 overlap.
RetrieveEmbeddings and cosine top-k over the chunk store.
AnswerStreamed answer with numbered citations, pluggable model providers, abstains when nothing matches.
Violet tint: parts I wrote. The rest existed before.
Engineering decisions
What was hard and what I did
One request instead of many
The customer detail page re-downloaded every list on each tab switch. A small list cache with a 60-second TTL now shares in-flight requests, so simultaneous calls resolve from one network round trip. Refresh buttons clear it.
Ask the backend for the right shape
A "last comment" column would have cost one request per customer, about 1,460 calls. I asked for one bulk field on the list endpoint instead.
Search that ignores Turkish letters
Customer names mix ı, İ, ş, ğ, ü, ö and ç with plain Latin spellings. Search folds both sides, so typing isik finds Işık.
Saving without losing data
Some legacy records carried dropdown values the new lookup lists did not know. The forms keep unknown values on save instead of silently wiping them, and never show raw enum IDs.
Evidence before blame
When users landed in the wrong tenant, the CRM migration was the first suspect. I pulled the tables touched, the triggers and the timeline in SQL and showed it was not the cause, which pointed the fix at the right place.
Retrieval tuned for Turkish
The Document Center answers only from retrieved passages, shows numbered sources and abstains when nothing relevant comes back, so staff can check every answer against the document.
Week-one decisions
The command palette and a Reduced / Full Motion switch were planned in the first week, not added at the end. Keyboard-first navigation and user-controlled animation shaped the render architecture, the store and the component APIs from the start.
Have a problem worth solving?Let's make it reliable