bottleneckcrm/mobile/docs/PARITY.md

9.9 KiB

Web and mobile parity

The tracker the CLAUDE.md files point at when a feature ships on one client and not the other. Add a Backlog row when that happens, and say so in the reply.

History

An earlier copy of this file was never committed, so it lived only in one working tree and was lost. It had started from 78 web pages against 26 mobile screens and, by 2026-08-08, recorded every page as built on both clients (create and edit share one form screen on mobile, so page counts differ on purpose). This file restarts the tracker from that point. It holds only gaps opened since, plus the surfaces that are web-only by design.

On 2026-09-26 the counts were 90 +page.svelte files under frontend/src/routes and 72 *_screen.dart files under mobile/lib. Page counts are not a parity measure: read the Backlog, not the totals.

How to use it

  • One row per gap. Say which client lacks what, and whether the backend already supports it.
  • A gap closes when both clients have it with tests. Move the row to Closed with the date instead of deleting it.
  • Recheck a row against the code before quoting it. Rows go stale when a fix lands without an edit here.
  • Parallel sessions edit this file. Re-read it right before editing and change exact rows, not whole sections.

Backlog

ID Gap Missing on Backend Opened Notes
B10 Download an invoice's PDF Mobile GET /api/invoices/{id}/pdf/ exists 2026-10-15 Found while building account statements (#810). The web's invoice page has Download PDF (routes/(app)/invoices/[id]/pdf/+server.js); the phone's invoice detail has no PDF action. The statement screen already saves its PDF through downloadAttachment, which is the pattern to reuse.
B13 Terminology on the phone Mobile terminology is served with the org settings and drives the web's nav and labels 2026-10-23 The phone shows default names for renamed objects (bar, More, lists). Found while building Modules, which already follows the row's label.
B15 Show the Salesforce import status's skipped object types Web and Mobile The Salesforce import status lists which object types its modules leave out 2026-10-23 Neither client has a Salesforce import screen, so there is nowhere to show it yet. Do this once such a screen exists. Found while building Modules.

Web-only by design

These are not gaps. Do not build them on mobile without a decision.

Surface Why
Public customer portal (routes/(no-layout)/portal/) Anonymous customers use it in a browser; it is not part of the staff app.
Public help center pages (routes/(no-layout)/help-center/) Anonymous, search-indexable pages for an org's customers, not app users. Both clients have the admin settings for it.
HTML and CSS editing in the invoice template editor Editing markup on a phone is not a real use; mobile edits the template's other fields.

Closed

ID Gap Closed How
B1 CSV import for contacts, tickets and leads 2026-09-26 One reusable sheet (mobile/lib/widgets/forms/csv_import_sheet.dart, provider csv_import_provider.dart) on the contacts, tickets and leads lists: pick, preview, commit, created count, template help, the web's 403 wording. The errors download stayed web-only (B4). Tracked as G1 in enterprise-crm/docs/gap-analysis/TRACKER.md.
B2 Editing a converted lead 2026-09-26 _buildPayload in lead_form_screen.dart now sends status on an edit only when it changed, as the web does, so editing any other field of a converted lead is no longer refused by LeadCreateSerializer.validate_status. The detail screen's sheets (assignees, tags, follow-up) never sent status. Tests: test/screens/leads/lead_form_status_test.dart.
B3 Parent and child tickets on the web 2026-09-26 Web /tickets/[id] has a Linked tickets panel: parent row (a redacted parent is shown, never linked), detach with an in-page confirm, the tree with the current ticket in bold, and a parent picker, gated on comment_permission. The unused TicketTreePanel.svelte and LinkParentDialog.svelte were deleted. Mobile's link action is gated the same way. Tests: tickets/[id]/tree.test.js, ticket_detail_redirect_and_gate_test.dart.
B4 CSV import errors file and valid-row sample on mobile 2026-09-26 csv_import_sheet.dart saves <plural>-import-errors.csv through file_picker (quoted and formula-guarded like the web's shared lib/utils/csv.js) and shows the first 20 valid rows.
B5 Document discount and tax on mobile invoice and estimate forms 2026-09-26 document_adjustments.dart: invoices set discount, tax and shipping; estimates and recurring schedules set discount and tax, in the web payload shape, with the server's bounds and messages mirrored.
B6 Mobile invoice detail breakdown 2026-09-26 Subtotal, Discount (when set), Tax and Shipping (when set) rows, as on the web. Neither client has an estimate detail screen.
B8 Web ticket detail: request approval, watch, saved-reply insert, link and unlink articles 2026-09-26 Found in the 1.12.0 pass (never had a row). A ticket under a pre-close approval rule could not be closed from the web at all. /tickets/[id] now has an approval panel gated on approval_rule, can_act, can_cancel and comment_permission, a Watch button on is_current_user_watching, a saved-reply picker that fills the composer and sends nothing, and an Articles card. Mobile's watch state and article suggestions were fixed in the same pass (they read keys the API never sent).
B9 Web Delete for leads, contacts, accounts, deals and solutions 2026-09-26 Found in the 1.12.0 pass (never had a row). Both clients now gate Delete on the server's can_delete; mobile's client-side admin-or-creator check (core/permissions.dart) is deleted.
B7 Deal board drag gated on the card's can_move 2026-09-27 The mobile deal board reads GET /api/opportunities/kanban/ (one call, with the list's pipeline and filters) instead of loading every page of the deals list, and a card drags or offers "Change stage" only when can_move is true (missing counts as false), like the ticket and lead boards. A column over the 100-card cap shows its real item_count. The board endpoint now accepts every deal list filter and its cards carry tags, line_items, stage_changed_at and updated_at. Tests: test/unit/deal_board_provider_test.dart, test/screens/deals/deals_list_screen_test.dart, opportunity/tests/test_kanban_card_mobile_fields.py.
B11 Done step after an import 2026-10-20 After a successful contacts or leads commit the phone sheet stays open on a done step, as the web's ImportFlow does: "Imported N contacts." with a check icon, one line per field the commit left blank (csvDroppedLines, by label), and "N rows skipped." with Save skipped rows, which saves the commit's own skipped rows (csvSkippedRowsCsv, formula-guarded, <plural>-skipped-rows.csv). CsvImportState.result keeps the commit's created, skipped and dropped. Done, Close, back and a tap outside all close it and run onImported, so the list refreshes and the welcome screen still lands on the imported list; the snackbar is gone for contacts and leads, and tickets keep theirs. Tests: test/unit/csv_import_provider_test.dart, test/widgets/csv_import_sheet_test.dart, test/widgets/import_welcome_screen_test.dart.
B12 @mention someone in a ticket comment, and show mentions as chips 2026-10-21 Web /tickets/[id]: after @ the composer offers active org members (MentionTextarea.svelte: keyboard or tap, a listbox announced through aria-activedescendant) and inserts @<email local part>, as the phone's picker does. Comments and notes draw a handle that names a member as a chip on both clients (MentionBody.svelte, mobile/lib/widgets/common/mention_text.dart); emails stay plain. Both mirror MENTION_RE, so a chip means the backend notified someone. Tests: frontend/src/lib/v2/mentions.test.js, mobile/test/widgets/mention_text_test.dart.
B14 Modules: switch features on and off 2026-10-23 Built on both clients from GET /api/org/modules/: Settings, Modules (switch off with the server's stops text, switch on, order within a group, the server's refusals with links to web forms and inbound email), menus, lists and screens that follow the switch, and a module-off view on a switched-off module's routes once the phone has reloaded the module list (a module_off answer, 404 or 409, triggers the reload). Phone: modules_screen.dart, module_gate.dart, nav_entries.dart; tests under test/screens/ (modules_surfaces_test.dart, shell_modules_test.dart, settings/modules_screen_test.dart, settings/modules_tablet_test.dart, phone_viewport_test.dart), test/unit/ and test/widgets/module_gate_test.dart. Deliberate differences: the bottom bar and More keep fixed labels (the phone reads no terminology yet, so a renamed module keeps its default name there; the Modules screen and the module view use the row's label); the bar fills with the first three of Leads, Deals, Tickets and Tasks that are on, where the web tab bar offers Pipeline, Tickets, Invoices, Leads and Tasks; a switch replaces the web's On and Off pill and button; the module view says its own sentence where the web shows the API's message, and its buttons read "Back to Home" and "Open Modules settings" where the web says "Back to Today" and "Open Settings, Modules"; the refusal links read "Web forms" and "Inbound email" where the web says "Open web forms" and "Open inbound email"; the web forms dialog ends "in Modules settings" where the web says "in Settings, Modules"; a paused automation rule shows its note with no "Open Modules" action, which the web's list and editor have; the settings hub hides the Modules row on an older server, where the web still shows it to admins, and shows no summary value beside it (the web shows 'All on' or 'N switched off'); no search and no floating button. The Salesforce import status is tracked in B15.