docs(uat): SHIPPED annotations for PR7 (Wave-2 polish batch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,7 +88,7 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
|
||||
> - **SHIPPED (lint guard only) in 52342ee:** `no-restricted-syntax` rule on `JSXText[value=/—/]` scoped to `src/components` + `src/app`, set to `warn`. 111 existing instances flagged as warnings — sweep remains parked.
|
||||
> - **Custom-field form: "Sort Order" needs an explainer tooltip — example of a broader gap** — _src/components/admin/custom-fields/custom-field-form.tsx:298-308_ — surfaces a specific instance of a platform-wide gap: see the next finding for the full sweep.
|
||||
> - **DocumentList DocRow kebab: add "Download" action** — _src/components/documents/document-list.tsx:86-109_ — current kebab has Send-for-Signing (draft only), Move-to-folder, Delete. No Download. Reps reviewing a signed doc from the interest's documents tab have to navigate into the document detail to download. Add a `<DropdownMenuItem>` at the top of the menu when `doc.signedFileId` is set (or `doc.fileId` for non-Documenso docs like manual uploads), wired to the same `apiFetch('/api/v1/files/[id]/download')` + anchor-click pattern used elsewhere. Permission-gate by `files.download` if that perm exists. ~10 min. Captured 2026-05-21 from UAT. **SHIPPED in 52342ee:** DocRow now renders Download at the top of the kebab when `signedFileId` is set; wired via the existing `triggerUrlDownload` helper from PR1.
|
||||
> - **InterestEoiTab "Open" link too ambiguous — relabel to "Open in Documents"** — _src/components/interests/interest-eoi-tab.tsx:163_ — the link in the EOI history list goes to `/${portSlug}/documents/${d.id}` (Documents Hub doc detail) but the label just says "Open" + an external-link icon. Rep can't tell where it goes until they hover. Change to `Open in Documents` (or `View in Documents`). Apply the same idiom anywhere else a cross-section navigation link uses bare "Open" — quick grep + sweep. ~5 min. Captured 2026-05-21 from UAT.
|
||||
> - **InterestEoiTab "Open" link too ambiguous — relabel to "Open in Documents"** — _src/components/interests/interest-eoi-tab.tsx:163_ — the link in the EOI history list goes to `/${portSlug}/documents/${d.id}` (Documents Hub doc detail) but the label just says "Open" + an external-link icon. Rep can't tell where it goes until they hover. Change to `Open in Documents` (or `View in Documents`). Apply the same idiom anywhere else a cross-section navigation link uses bare "Open" — quick grep + sweep. ~5 min. Captured 2026-05-21 from UAT. **SHIPPED in c6dcf49.**
|
||||
> - **PaymentsSection: deprioritize layout — move below milestones + collapse-by-default at Reservation** — _src/components/interests/interest-tabs.tsx:633 + 846-852_ (current `showPaymentsSection = reservationStageReached` + renders at line 847, ABOVE the milestone strip at line 859+) + _src/components/interests/payments-section.tsx_ (the section component itself). Today: hidden pre-Reservation (correct ✓), shows as a full expanded card at Reservation+ sitting above the milestone strip. Section is reference/history once expected — milestone work (active step) should be the rep's primary visual focus, not deposits-tracking.
|
||||
> - **Fix (three states):**
|
||||
> - **Pre-Reservation:** keep hidden (no change).
|
||||
@@ -102,6 +102,7 @@ _Copy tweaks, alignment, single-prop edits, obvious typos._
|
||||
> - **Backend:** swap the response payload's `interest: { id, clientName }` → `interest: { id, berthLabel }` where `berthLabel` is derived in the service layer from the interest's primary or in-bundle berths. Falls back to "No berths linked" when no berths are attached.
|
||||
> - **Frontend:** change line 241 from `sub: linked.interest.clientName` → `sub: linked.interest.berthLabel ?? 'No berths linked'`.
|
||||
> - **Effort:** ~15-20 min including type updates + a vitest covering the multi-berth + no-berths paths. Captured 2026-05-21 from UAT. Cross-ref: pairs with the shared title-derivation helper note in the external-EOI bundle (Bucket 2) — single `deriveBerthLabel(interest)` helper used everywhere.
|
||||
> - **SHIPPED in c6dcf49:** documents.service derives `berthLabel` from `interest_berths` (in-EOI-bundle subset → primary → all linked), `DocumentDetailLinkedEntities` shape gains `berthLabel`, frontend renders `linked.interest.berthLabel ?? clientName ?? 'No berths linked'`.
|
||||
> - **Platform-wide `<FileInputButton>` primitive — replace 7 raw `<Input type="file">` instances with native browser-default styling** — _new_ `src/components/ui/file-input-button.tsx` + sweep — `<input type="file">` rendered without a wrapper shows the browser-default "Choose File / No file chosen" UI, which looks raw and inconsistent across Chromium / Safari / Firefox / Comet. We already use the correct idiom in `expense-form-dialog.tsx:389` (Button + hidden input + filename row) and `file-upload-zone.tsx`, but 7 other call sites still use the raw pattern.
|
||||
> - **Affected files:** `external-eoi-upload-dialog.tsx:92`, `template-editor.tsx:486 + 526`, `brochures-admin-panel.tsx:213`, `berth-documents-tab.tsx:176`, `won-status-panel.tsx:200`, `pdf-logo-uploader.tsx:278`, `settings-form-card.tsx:486`.
|
||||
> - **Component shape:** `<FileInputButton accept={...} multiple={...} onFilesPicked={(files) => ...} label="Upload PDF" icon={<Upload />} variant="outline" size="sm" />`. Renders a styled Button (Upload icon + label) + hidden `<input type="file">` underneath. Optional: after-pick filename row with X to clear, mirroring the expense form's pattern.
|
||||
@@ -402,7 +403,7 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
|
||||
> - **Fix:** replace the OverviewTab notes block (lines 227-236) with `<NotesList entityType="yachts" entityId={yachtId} currentUserId={currentUserId} />`. The `yachtNotes` table already exists (per CLAUDE.md polymorphic notes architecture: `notes.service.ts` dispatches across `clientNotes`/`interestNotes`/`yachtNotes`/`companyNotes`) so no backend work.
|
||||
> - **Legacy `yachts.notes` column:** verify (a) anything else writes it (other than this textarea); (b) anything reads it (EOI / contract / template merge fields). If unused elsewhere, deprecate the column and stop surfacing it on Overview; the threaded NotesList becomes the canonical write path. If still in use, leave the column but stop surfacing on Overview.
|
||||
> - **Companion decision:** with NotesList on Overview, the dedicated Notes tab may become redundant — same tradeoff applies to clients/interests today. Defer that decision; ship the inline NotesList first.
|
||||
> - **Effort:** ~30 min for the swap + verify `currentUserId` is plumbed through to OverviewTab. Captured 2026-05-18 from UAT.
|
||||
> - **Effort:** ~30 min for the swap + verify `currentUserId` is plumbed through to OverviewTab. Captured 2026-05-18 from UAT. **SHIPPED in c6dcf49:** OverviewTab now renders `<NotesList entityType="yachts" parentInvalidateKey={['yachts', yachtId]}>`; `currentUserId` plumbed through. Legacy `yacht.notes` column retained for EOI/contract merge-field path; decision on the dedicated Notes tab deferred.
|
||||
> - **`/invoices/upload-receipts` guide: copy rewrite — terse, professional, in the luxury-CRM voice** — _src/components/invoices/upload-receipts-guide.tsx_ (the whole page; ~190 lines, ~75% of which is body copy) — current copy reads like a friendly onboarding tutorial: hand-holding ("Snap a photo of the receipt with your phone"), explanatory tangents ("The behind-the-scenes part is called OCR..."), throwaway pleasantries ("Most of the time everything is correct", "No typing. No spreadsheets. No chasing finance for the form."), and parenthetical asides ("the square with the arrow pointing up"). Tone is out of step with the rest of the CRM — the platform's brand voice is precise, restrained, declarative; this page reads warm-blog. Rewrite passes:
|
||||
> - **PageHeader description** (line 31) — currently `"When you spend your own money on a business expense for the marina, use this to log it. Snap a photo of the receipt with your phone, the system reads it for you, and finance approves it on the parent company's side."` → suggested: `"Capture out-of-pocket expenses for reimbursement. The system extracts vendor, date, total, and currency from each receipt and routes the claim to finance."`
|
||||
> - **"What does it actually do?" section** (lines 51-65) — replace title with `"Overview"`. Replace the two paragraphs with one line: `"Submit a photographed receipt; the system populates the expense form via OCR with AI-assisted field extraction, then forwards the claim for parent-company approval."` Drop the OCR explanation entirely — the audience is internal staff, not customers.
|
||||
@@ -412,7 +413,7 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
|
||||
> - **Target length:** ~60-70% reduction. Reads in 30 seconds instead of 3 minutes; the rep gets the workflow, not a friendly essay.
|
||||
> - **Companion audit:** flag for review across other guide / help / empty-state copy that may have drifted into the same warm-blog voice (consumers of `src/components/shared/empty-state.tsx`, any `*-guide.tsx` pages, onboarding flows, longer Toast copy). One pass for tone consistency platform-wide — captured as a deferred follow-up; this page is the most visible offender.
|
||||
> - **Effort:** ~45 min for this page; ~3-4h for the platform-wide tone audit if pursued. Captured 2026-05-18 from UAT.
|
||||
> - **Expenses page header copy: drop "port" from the description** — _src/app/(dashboard)/[portSlug]/expenses/page.tsx:33_ → PageHeader at _src/components/shared/page-header.tsx:38_ — description currently reads `"Track and manage port expenses"`; user wants the word `"port"` removed. Suggested copy: `"Track and manage expenses."` (or, if the team wants to keep the "manage" verb spelled out, `"Track and manage business expenses."`). Trivially small. ~30 sec. Captured 2026-05-18 from UAT — likely indicative of a broader "remove the word 'port' from user-facing copy where it's redundant" pass; the portSlug already scopes everything, so user-facing strings shouldn't restate it. Worth a quick grep for `port expenses`, `port clients`, `port settings`, etc. in component strings.
|
||||
> - **Expenses page header copy: drop "port" from the description** — _src/app/(dashboard)/[portSlug]/expenses/page.tsx:33_ → PageHeader at _src/components/shared/page-header.tsx:38_ — description currently reads `"Track and manage port expenses"`; user wants the word `"port"` removed. Suggested copy: `"Track and manage expenses."` (or, if the team wants to keep the "manage" verb spelled out, `"Track and manage business expenses."`). Trivially small. ~30 sec. Captured 2026-05-18 from UAT — likely indicative of a broader "remove the word 'port' from user-facing copy where it's redundant" pass; the portSlug already scopes everything, so user-facing strings shouldn't restate it. Worth a quick grep for `port expenses`, `port clients`, `port settings`, etc. in component strings. **SHIPPED in c6dcf49:** "port" dropped. Platform-wide grep sweep deferred to follow-up.
|
||||
> - **Topbar search: widen + center against the _viewport_ (including sidebar space), not the topbar's middle grid slot** — _src/components/layout/topbar.tsx:57_ (grid template) + _src/components/layout/topbar.tsx:77-84_ (search container) + _src/components/search/command-search.tsx:103_ (the input itself) + _src/app/globals.css:114_ (`--width-sidebar: 256px` token already available) — current behaviour: the topbar uses `grid grid-cols-[minmax(0,1fr)_minmax(360px,640px)_minmax(0,1fr)]` inside the AppShell's main area (right of the sidebar), so the search bar centers within _the topbar_ — visually it sits offset to the right of the screen by half the sidebar width because the topbar itself starts after the sidebar. User wants the search visually centered against the full viewport (sidebar inclusive) and wider.
|
||||
> - **Two coordinated changes:**
|
||||
> - **(a) Wider:** bump the search container's `max-w-md` (448px) at line 81 to `max-w-2xl` (672px) or `max-w-3xl` (768px), and bump the topbar grid's middle slot from `minmax(360px,640px)` to `minmax(420px,800px)`. Cap to whatever still leaves room for the left breadcrumbs + right action row on common laptop widths (1280px - 256px sidebar = 1024px main area minus padding). 672-720px is a comfortable upper bound.
|
||||
@@ -442,15 +443,15 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
|
||||
> - **(a) Dock letters are free-form / inferred from `berths.mooring_number`** (no separate `docks` table): then the wizard just needs to allow a new letter prefix in its input. UI fix: replace the letter input (or dropdown) with a combobox-style "pick existing or type a new letter" control — same idiom as Tag picker. Backend: nothing — first insert with the new prefix establishes the dock. ~30 min.
|
||||
> - **(b) Docks are a first-class entity** (separate `docks` table with `port_id` + `letter` + metadata like `position`, `pontoon_type`, `power_capacity`): then the wizard needs a "+ New dock" affordance opening a small dock-create dialog (letter + name + optional metadata), then returning to the wizard with the new dock pre-selected. Permission: `berths.manage_docks` (or whichever owns dock metadata). The user's question — "_or is this an admin setting?_" — suggests they're not sure either; if it IS an admin-only concern (docks are infrastructure not data the rep should touch), then keep it admin-side and just surface a contextual link in the wizard ("New dock? Add it in Admin → Docks first → [link]"). ~1-2h depending on the model.
|
||||
> - **Action item:** check whether `docks` / `pontoons` / `marina_sections` table exists in the schema (`grep -r "docks\|pontoons" src/lib/db/schema/`); shape the fix accordingly. If no dedicated table, the wizard fix is trivial; if there is one, decide admin-only vs in-wizard-create with the team. Captured 2026-05-18 from UAT.
|
||||
> - **DropdownMenu content stretches to fill viewport — cap it** — _src/components/ui/dropdown-menu.tsx:66_ — the shadcn `DropdownMenuContent` primitive uses `max-h-(--radix-dropdown-menu-content-available-height)` (Radix's CSS variable that exposes the room between the trigger and the viewport edge). On long lists the menu visually stretches all the way to the viewport bottom even though the items don't need that height; reads as a wall of options. Internal `overflow-y-auto` is already on so scrolling works. Fix: replace the Radix `max-h-(...)` token with a fixed `max-h-96` (384px) or `max-h-[28rem]` (448px) so the menu caps at a comfortable height regardless of available space, scrolling internally for longer lists. Global change in the base primitive — affects every dropdown in the app, which is the right call (no consumer currently relies on the "fill the viewport" behaviour). ~2 min. If a specific dropdown needs the old behaviour, it can pass `className="max-h-[var(--radix-dropdown-menu-content-available-height)]"` to opt back in. Captured 2026-05-18 from UAT.
|
||||
> - **DropdownMenu content stretches to fill viewport — cap it** — _src/components/ui/dropdown-menu.tsx:66_ — the shadcn `DropdownMenuContent` primitive uses `max-h-(--radix-dropdown-menu-content-available-height)` (Radix's CSS variable that exposes the room between the trigger and the viewport edge). On long lists the menu visually stretches all the way to the viewport bottom even though the items don't need that height; reads as a wall of options. Internal `overflow-y-auto` is already on so scrolling works. Fix: replace the Radix `max-h-(...)` token with a fixed `max-h-96` (384px) or `max-h-[28rem]` (448px) so the menu caps at a comfortable height regardless of available space, scrolling internally for longer lists. Global change in the base primitive — affects every dropdown in the app, which is the right call (no consumer currently relies on the "fill the viewport" behaviour). ~2 min. If a specific dropdown needs the old behaviour, it can pass `className="max-h-[var(--radix-dropdown-menu-content-available-height)]"` to opt back in. Captured 2026-05-18 from UAT. **SHIPPED in c6dcf49.**
|
||||
> - **DocumentsHub aside column: flush-left with the app sidebar (kill the AppShell padding for this page)** — _src/components/documents/documents-hub.tsx:246_ + _src/components/layout/app-shell.tsx:113-121_ — the desktop `<main>` wrapper applies `px-6 pt-3 pb-6` to all dashboard pages, so the DocumentsHub two-pane (`ResizablePanelGroup` with the `<aside>` folder column on the left) gets 24px of whitespace between the global app sidebar and its own border. The folder column should sit flush against the app sidebar — it reads as "an extension of the navigation," not "a card inside the page." Fix (surgical): change DocumentsHub's root `<div className="h-full">` at line 246 to `<div className="h-full -mx-6 -mt-3 -mb-6">` (mirror the AppShell desktop padding so the hub renders full-bleed inside the main viewport). Add a comment explaining the intentional escape. The right-pane content keeps its own internal `p-4` so it doesn't run flush with the viewport edge. **Alternative (cleaner long-term):** make the AppShell padding route-aware via a prop on `<main>` (or a layout-level opt-out for hub-style pages); but (a) is the right call until a second page needs the same treatment. ~5 min for the negative-margin fix. Captured 2026-05-18 from UAT.
|
||||
> - **DocumentsHub: hide breadcrumb on root "All documents" view, move PageHeader up to fill the space** — _src/components/documents/documents-hub.tsx:196-209_ — the top row currently always renders the `FolderBreadcrumb` (and conditionally the `NewDocumentMenu` when a folder is selected); on the root view (`selectedFolderId === undefined`) the breadcrumb shows only a "Home / All documents" label with no useful navigation, eating vertical space above the `PageHeader` that already says "Documents" + description. Fix: wrap the entire breadcrumb row at line 196-209 in `{selectedFolderId !== undefined && ( … )}` so the row is gone on the root; the PageHeader becomes the top element. When the rep navigates into a folder, the row reappears with both breadcrumb + NewDocumentMenu (the existing folder views don't render PageHeader, so the breadcrumb is the wayfinding cue). ~5 min. Captured 2026-05-18 from UAT.
|
||||
> - **Residential InterestsTab: whole row should navigate to the interest, not just the "View" link** — _src/components/residential/residential-client-tabs.tsx:273-289_ — current `<li>` lays out `[stage chip] [preferences/notes truncated text] [View → link]` and only the "View" text on the right is clickable. The whole row should be a target, matching the idiom used in the main client's `InterestRowItem` (`src/components/clients/client-interests-tab.tsx:53`) — the entire card is a `<button>`/`<Link>` so reps can tap anywhere. Fix: wrap the `<li>`'s flex container in `<Link href={…}>` (`className="block w-full"` to preserve layout), drop the trailing "View" link, add `hover:bg-muted/50` to make the affordance discoverable. ~10 min. Captured 2026-05-18 from UAT.
|
||||
> - **Residential InterestsTab: whole row should navigate to the interest, not just the "View" link** — _src/components/residential/residential-client-tabs.tsx:273-289_ — current `<li>` lays out `[stage chip] [preferences/notes truncated text] [View → link]` and only the "View" text on the right is clickable. The whole row should be a target, matching the idiom used in the main client's `InterestRowItem` (`src/components/clients/client-interests-tab.tsx:53`) — the entire card is a `<button>`/`<Link>` so reps can tap anywhere. Fix: wrap the `<li>`'s flex container in `<Link href={…}>` (`className="block w-full"` to preserve layout), drop the trailing "View" link, add `hover:bg-muted/50` to make the affordance discoverable. ~10 min. Captured 2026-05-18 from UAT. **SHIPPED in c6dcf49.**
|
||||
> - **Residential namespace breadcrumb link is 404** — _src/components/layout/breadcrumbs.tsx_ (the breadcrumb generator splits the URL and makes every segment a link) + missing _src/app/(dashboard)/[portSlug]/residential/page.tsx_ — on any `/{portSlug}/residential/clients` or `/{portSlug}/residential/interests` page, the breadcrumb renders "Residential" as a link to `/{portSlug}/residential` but no `page.tsx` exists at that path (only `clients/` and `interests/` subdirectories). Clicking the breadcrumb yields a 404. Two reasonable fixes:
|
||||
> - **(a) Quickest:** create `src/app/(dashboard)/[portSlug]/residential/page.tsx` as a server component that calls `redirect(`/${portSlug}/residential/clients`)`. Single file, ~5 min, breadcrumb works immediately. Same pattern works for any other namespace-only segment that lacks a real landing page.
|
||||
> - **(b) Cleaner long-term:** add a "namespace" concept to the breadcrumb generator — segments that exist only as URL parents (residential, admin if applicable, …) render as plain text (`<BreadcrumbPage>`) rather than `<BreadcrumbLink>`. Centralized in `breadcrumbs.tsx`'s `SEGMENT_LABELS` map by extending the value to `{ label, namespace?: boolean }`. ~30 min, fixes the class of problem instead of one instance.
|
||||
> - **Recommendation:** ship (a) now, queue (b) if/when a second namespace-only segment hits the same issue.
|
||||
> - Captured 2026-05-18 from UAT.
|
||||
> - Captured 2026-05-18 from UAT. **SHIPPED (a) in c6dcf49:** new `src/app/(dashboard)/[portSlug]/residential/page.tsx` server-redirects to `/${portSlug}/residential/clients`. (b) namespace concept queued for the second-instance case.
|
||||
> - **Residential client detail header: match the main ClientDetailHeader layout** — _src/components/residential/residential-client-detail-header.tsx_ vs _src/components/clients/client-detail-header.tsx_ — the main client header is rich (`Email` / `Call` / `WhatsApp` deeplink button row using primary contact channels, `PortalInviteButton`, `GdprExportButton`, tag chips, top-right action menu with Bell/reminder + Archive/Restore state-aware + perm-gated hard-delete, archived badge with conditional dialog routing). The residential header (33 lines vs 244) shows only an eyebrow, an inline-editable name, a status badge, and place-of-residence — visually orphaned next to the main client experience.
|
||||
> - **Data-model gap to bridge:** residential clients store contacts inline (`email`, `phone`, `phoneE164`, `phoneCountry` columns on `residentialClients`) rather than via the polymorphic `clientContacts` table the main model uses. Action buttons can still be wired by synthesizing a `[{ channel: 'email', value, isPrimary: true }, { channel: 'phone', value: phone, valueE164, isPrimary: true }]` shape from the inline columns. Other features need verification per residential: tags table exists? portal invite (`residential_clients` has no `clientPortalEnabled` flag → likely N/A); GDPR export (yes — applies to any natural person in EU residence; need a `residential-gdpr-export` route if not already there); archive/restore (residential uses its own service; verify the dialog component expects a `residentialClientId` or needs a separate `ResidentialSmartArchiveDialog`).
|
||||
> - **Approach options:**
|
||||
|
||||
Reference in New Issue
Block a user