7.9 KiB
"Ideal Web App" Pass — Edit Everywhere, Dialogs, Toasts, Settings Hub
Approved 2026-07-17 (Approach A). Fixes the six gaps that keep the console feeling like a
wireframe despite the D17 restyle: no record editing, browser window.prompt/confirm
popups, silent success, no Settings home, inline wireframe forms, no favicon. Visual
reference: Pinterest's product chrome — centered rounded modals with generous padding and
pill buttons, and a left-rail settings page.
1. Goal & scope
Make every flow feel like a finished web application. Mostly UI; two small additive
backend slices (reminder-schedule endpoints, sharing default). No schema changes beyond
zero (settings ride the existing setting table; cadence changes are new dated
reminder_schedule rows per rule 3). @sims/ui stays generic.
Key server fact: PATCH /clients/:id already updates name / gstin / stateCode /
address / contacts / status / notes (repos-clients.ts:85) — client editing is pure UI.
updateAmc, updateRecurringPlan, updateInteraction, patchModule, patchEmployee
likewise exist and are under-used by the UI.
2. App-chrome kit (packages/ui — generic only)
| Piece | Contract |
|---|---|
Dialog |
Pinterest modal. Props: open, onClose, title, size?: 'sm'|'md'|'lg' (420/560/720px max-width), footer?: ReactNode, children = body (scrolls if tall). 16px radius, --shadow-lg, scrim (blur + dim) closes on click; Esc closes; focus trap + focus restore (same technique as CommandPalette); role="dialog" aria-modal aria-label={title}; ✕ button in the title row. Replaces the legacy Modal (currently zero call sites — delete it). |
ConfirmDialog |
On top of Dialog (size='sm'): open, onClose, onConfirm, title, body?, confirmLabel?, tone?: 'danger'. Confirm button is ink (or danger red); busy state while onConfirm's promise resolves. Kills every window.confirm. |
Toaster |
ToastProvider (mounted once in main.tsx) + useToast() → toast.ok(msg), toast.err(msg). Bottom-right stack, max 3 visible, 4s auto-dismiss + ✕, aria-live="polite", ok/err tones from tokens. Pure state logic in an exported reducer (toast-store.ts) so it's unit-testable without a DOM. |
FormGrid |
display:grid; grid-template-columns: 1fr 1fr; gap 12px (single column ≤560px); fields span-2 via a wide prop on a FormField wrapper (thin wrapper over existing Field). |
Button pill |
pill?: boolean prop → 999px radius (Pinterest button shape) — used in dialog footers only. |
CSS: new wf-dialog*, wf-toast*, wf-formgrid blocks in tokens.css; z-order — drawer
40 < modal-back 50 < palette 60 < toasts 70.
3. Edit everywhere (all modals; toast on success; ConfirmDialog on destructive)
| Surface | Modal content | Endpoint (exists) |
|---|---|---|
| Client 360 header → Edit | name · GSTIN · state · address · notes + contacts repeater (name/email/phone/role rows, add/remove, ≥0 rows) | PATCH /clients/:id |
| Clients → New client | same form, empty; ?new=1 opens it |
POST /clients |
| AMC row → Edit / new | coverage, period, amount ₹, reminder days | PATCH /amc/:id, create |
| Plan row → Edit / new | module, cadence, amount, next run, policy | PATCH /recurring/:id, create |
| Interaction row → Edit / log | type, date, outcome, follow-up, notes | PATCH /interactions/:id, create |
| Employees add / edit / reset-pw | as today's inline forms | existing |
| Modules new / add-price | as today's inline forms | existing |
| Deactivate (AMC/plan/employee) | ConfirmDialog, danger tone | existing |
Quote-content editor stays inline (it has the live preview pane). Inline Notice remains
only for in-form validation; all mutation success/failure also lands a toast.
4. Send-by-email modal + document confirms
- Send (DocumentView): Dialog with To —
<select>of the client's contacts that have emails + an "other…" free-text option — and Subject prefilled${docType} ${docNo}. Gmail state inline: connected → send enabled; disconnected/dead → disabled with the reconnect hint. Calls the existingdocumentAction(id,'send',{to,subject}). - Convert-to-invoice-and-send, supersede, cancel, credit-note: ConfirmDialog with the real
consequence copy (number consumption, email dispatch). All six
window.*popups die.
5. Settings hub — /settings (Pinterest left rail + one panel)
Owner-only except Appearance. Rail sections; active section in ?s= (deep-linkable).
| Section | Panel | Backend |
|---|---|---|
| Company | profile form (GSTIN checksum + state code validated server-side) | GET/PUT /settings/company (exists) |
| Document template | today's DocumentTemplate editor moved in whole (form + live preview); old route /settings/template → Navigate to /settings?s=template |
exists |
| Reminders | read-only table of reminder_schedule rows (rule kind · effective from/to · day offsets · subject) + “New dated row” modal (rule kind, effective_from, offsets CSV validated 3,7,14-style, subject, body). Rows are never edited or deleted — a change is a new row (rule 3). Also two number fields: overdue days / renewal days. |
new GET /settings/reminders → { overdueDays, renewalDays, schedules[] }; PUT /settings/reminders → the two day settings; POST /settings/schedules → insert row. Owner-gated, audited, transactional. |
| Sharing | default share-link expiry days (blank = never) | new GET/PUT /settings/sharing → setting row share.default_expiry_days; createShare reads it as the default when the caller passes nothing (fallback 30 unchanged). |
| Integrations | Gmail: /email/status (connected/address/dead + reconnect hint). AWS: last month's ranking total via existing GET /aws/ranking (or "no data"). Read-only. |
exists |
| Appearance | theme mode + accent swatches (moves the full picker here; the compact top-bar toggle stays) | none |
Nav: ADMIN group = Settings (owner-only; replaces the Document Template item) +
Employees. Doc Template breadcrumb/palette entries update accordingly.
6. Polish
- Favicon: teal rounded-square SVG with "HQ" mark (
apps/hq-web/public/favicon.svg+ link inindex.html); same mark reused as the login brand accent. document.titleper route:"<Page> · SiMS HQ"set inLayoutfrom the nav map (record pages: doc no / client name once loaded).- Errors from mutations surface as
toast.err; page-level fetch failures keepErrorState(unchanged).
7. Explicitly not doing
Masonry/card re-skins of tables (Tables Rule); editing issued documents (immutability); schedule-row edit/delete (dated rows are append-only); WhatsApp/send-channel changes; password-protected shares; drawer pattern (modal chosen); i18n.
8. Testing & verification
- Kit:
toast-storereducer + offsets-CSV validator get pure vitest tests; Dialog focus logic reuses the proven palette pattern (no DOM env in repo — behavior verified e2e). - Backend:
settings-reminders.test.ts+settings-sharing.test.tsinapps/hq/test(real-DB style like siblings): schedule insert appends + audits + never mutates, day settings round-trip,resolveSchedulepicks the new dated row from its effective date,createSharehonors the sharing default and the explicit param still wins. - Gates: typecheck + full suite (293 + new) green; e2e walk — edit client (name + contact add), send modal both Gmail states, one ConfirmDialog per document action, every settings section round-trip, toasts, favicon/tab titles, dark mode + 420px pass.
9. Build order
- Kit (Dialog/Confirm/Toaster/FormGrid/pill + CSS + toast tests)
- Backend slices (reminders + sharing endpoints, tests)
- Send modal + document ConfirmDialogs
- Client edit + New-client modal
- Remaining edit/create modals (AMC, plans, interactions, employees, modules)
- Settings hub + nav/route changes
- Favicon + titles + toast sweep
- Verification pass (§8)