docs(uat): annotate master doc for Group B ships (7ecf4ee)
5 master-doc entries now carry the `SHIPPED in 7ecf4ee` line:
- Interest Overview Email + Phone contact picker (Design A)
- Inline phone editor on the Contact row (reuses InlinePhoneField)
- Client Overview interest summary (Wants L × W × D · Source)
- InterestBerthStatusBanner names + links competing deal
- Notes Latest-note teaser stage pill (current-stage variant)
2 entries already shipped / no annotation needed:
- B13 (Inbox embedded filter) — pre-shipped, marker already present
- B19 (intent auto-confirm on EOI+) — already shipped in 51ca875
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,9 +210,10 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
|
||||
> - **Design B (per-interest contact override):** add `interests.preferred_email_contact_id` + `preferred_phone_contact_id` nullable FK to a specific `client_contacts` row. Each interest can pin a non-primary contact for itself; falls back to client's primary when null. Schema change + service-layer fallback logic + UI to mark "use this for this deal only". Useful only if a single client routinely buys multiple deals with different contact preferences per deal — uncommon for marina sales.
|
||||
> - **Decision-pending:** lean Design A unless leadership confirms the multi-deal-per-client divergence case is real.
|
||||
> - **Effort:** ~3-4h for Design A end-to-end (picker component + empty-state quick-add + service-side `setPrimary` action + tests + accessibility). ~5-7h for Design B with the schema + fallback logic. Captured 2026-05-21 from UAT.
|
||||
> - **Inline phone editor on the Contact row** — _src/components/interests/interest-tabs.tsx:973_ — current implementation uses a plain `InlineEditableField` text variant on Phone, so reps can't pick a country code from a dropdown or get AsYouType formatting (both available via `<PhoneInput>` in `src/components/shared/phone-input.tsx`). Wrap `PhoneInput` in a display-vs-edit toggle and PATCH both `value` (national string) + `valueE164` + `valueCountry` to `/api/v1/clients/{id}/contacts/{contactId}`. ~30-60 min.
|
||||
> - **SHIPPED (Design A) in 7ecf4ee:** new `<ClientChannelEditor>` combobox. Primary value renders inline (free-text for email, `<InlinePhoneField>` for phone with country code split). Chevron opens a popover listing every contact in the channel — `Make primary` button per non-primary row, delete for non-primaries, inline "Add an email / phone number" with optional Set-as-primary toggle. Backed by existing `/clients/[id]/contacts` CRUD + `promote-to-primary`. Wired into the Email + Phone rows on `interest-tabs.tsx`.
|
||||
> - **Inline phone editor on the Contact row** — _src/components/interests/interest-tabs.tsx:973_ — current implementation uses a plain `InlineEditableField` text variant on Phone, so reps can't pick a country code from a dropdown or get AsYouType formatting (both available via `<PhoneInput>` in `src/components/shared/phone-input.tsx`). Wrap `PhoneInput` in a display-vs-edit toggle and PATCH both `value` (national string) + `valueE164` + `valueCountry` to `/api/v1/clients/{id}/contacts/{contactId}`. ~30-60 min. **SHIPPED in 7ecf4ee:** the phone branch of `<ClientChannelEditor>` uses `<InlinePhoneField>` (existing primitive); PATCH writes `value` / `valueE164` / `valueCountry` together. `interests.service.ts` now returns `clientPrimaryPhoneCountry` so the editor can preserve the ISO-3166-1 alpha-2 round-trip.
|
||||
> - **ft ↔ m unit switching on Berth Requirements** — _src/components/interests/interest-tabs.tsx_ — the three inline-editable dim rows hard-code `(ft)` in the label. The interest already carries `desiredLengthUnit` ('ft' | 'm'); other surfaces (BerthRecommenderPanel) honour it. Add a small unit toggle that flips the rendered display (and converts on save so the canonical `desired*Ft` column stays in feet). Same pattern as elsewhere in the app (per CLAUDE.md mooring/berth dims model). ~30-45 min.
|
||||
> - **Client Overview should summarize current interest's requirements** — _src/components/clients/_ — one-line "current interest needs X × Y × Z" summary on the client detail Overview tab; reps currently have to drill into Interests tab to see what a client wants. ~30 min.
|
||||
> - **Client Overview should summarize current interest's requirements** — _src/components/clients/_ — one-line "current interest needs X × Y × Z" summary on the client detail Overview tab; reps currently have to drill into Interests tab to see what a client wants. ~30 min. **SHIPPED in 7ecf4ee:** `PanelVariant` of `<ClientPipelineSummary>` renders a one-line "Wants L × W × D · Source" under each interest's header when constraints / source are captured. `<ClientInterestRow>` type extended with the new fields; the existing `/api/v1/interests` query already returned them.
|
||||
> - **Duplicate Reminder surfaces on Interest Overview** — _src/components/interests/interest-tabs.tsx_ — the legacy "Reminder" panel (driven by `interest.reminderEnabled / reminderDays / reminderLastFired`) and the new "REMINDERS" section (driven by the `reminders` table via the bell-in-header) both render on the same tab and tell different stories. The legacy field still drives a real backend worker (`processFollowUpReminders` in `reminders.service.ts:428` — creates auto-follow-up reminders when no activity in N days), so we can't just delete the field. Approach: hide the legacy "Reminder" panel from the OverviewTab grid; surface the recurring-follow-up config either as a slim row inside the REMINDERS section or as a setting on the interest detail header. Keep the worker untouched. ~1 h. **SHIPPED in f39f0aa:** legacy panel hidden from Overview; worker untouched. Surfacing the recurring-follow-up config on the detail header is parked.
|
||||
> - **LinkedBerthsList: no "add another berth" affordance from the card** — _src/components/interests/linked-berths-list.tsx_ — multi-berth interests are first-class (`interest_berths` is the source of truth per CLAUDE.md) but the LinkedBerthsList card doesn't expose an inline "Add a berth" button. Reps have to use the BerthRecommenderPanel below — discoverability gap. Add a CTA button to the card header (gated by `berths.edit`) that opens a picker / sheet to add another `interest_berths` row. ~45 min. **SHIPPED in 3999d4b:** "Add berth" button on the card header; opens a `<Dialog>` with a Command-primitive searchable picker backed by `/api/v1/berths/options`. Berths already linked are filtered out client-side so reps can't double-add. Mutation hits `POST /api/v1/interests/[id]/berths` with `isSpecificInterest` flag; invalidates interest-berths + berth-recommendations caches so the row appears immediately and the recommender drops the just-added berth.
|
||||
> - **Supplemental-info-request: link should be reusable, not single-use** — _src/lib/services/supplemental-info_ (token model) — current email says "can only be used once"; user wants it valid until expiry so a partial submission can be revisited. Drop the single-use guard, keep TTL gate. Audit the public endpoint to ensure no token-fingerprint reuse risk before lifting the limit. ~30 min. **SHIPPED in b74fc56:** `applySubmission` drops the `isNull(consumedAt)` filter; TTL is the sole validity check. Public form's "already submitted" lockout screen replaced with a soft amber banner noting that re-submission overwrites the previous data. `consumedAt` still stamped for last-submitted context.
|
||||
@@ -224,8 +225,8 @@ _Component refactors, multi-file edits, single-service tweaks, new validators._
|
||||
> - **Effort:** ~2-3h end-to-end including the service split, two API routes, UI rework, audit-log entries on each action, and a vitest covering the resend-doesn't-mutate-token guarantee. Captured 2026-05-21 from UAT. Cross-ref: ties into the "link should be reusable, not single-use" + "separate generate link and send email" findings — best done as one coherent rework.
|
||||
> - **Supplemental-info-request: separate "generate link" and "send email"** — _src/components/interests/supplemental-info-request-button.tsx_ — currently one button auto-generates + sends. User wants two steps: button 1 generates + shows the link (rep can copy / share manually); button 2 sends the templated email through SMTP. Backend change: split the existing service into `generateSupplementalLink()` and `sendSupplementalLinkEmail(linkId)`. UI change: replace single-click action with two-step UI showing link state. ~1 h. **SHIPPED in a4e30ea:** API route now accepts `{ sendEmail?: boolean }` (defaults true for back-compat); UI shows two distinct buttons — "Generate link" and "Send by email" (becomes "Regenerate link" + "Generate + email" depending on state). Email body copy also drops the "can only be used once" sentence since PR15 made tokens reusable.
|
||||
> - **Past-milestones strip → expandable history with inline doc preview** — _src/components/interests/interest-tabs.tsx_ (the past-milestones strip at ~line 863) — currently a one-line collapsed summary per past milestone (just title + summary). Reps want to drill into the history of a specific milestone (e.g. see which EOI round was signed, the doc contents, who signed, when). Convert the strip into an accordion: each past milestone expands to show its associated docs + sub-status timeline + inline PDF preview using the existing pdf viewer primitive. Useful for deals with multiple EOI rounds (rework after rejection, re-sent reservation agreements, etc.) where audit trail matters. ~3-4 h.
|
||||
> - **InterestBerthStatusBanner: name + link the competing deal** — _src/components/interests/interest-berth-status-banner.tsx_ — the banner that surfaces when a linked berth is under offer to a different active deal currently just says "this berth is under offer elsewhere" without identifying which interest. Reps want a small inline detail: client name + deal stage + a link button to the competing interest, so they can size up the situation (e.g. "this lead won't make it, treat ours as backup"). Service-side: extend the `getInterestBerthStatus()` (or equivalent) response with a `competingInterest: { id, clientName, pipelineStage, ... } | null` field, then surface in the banner. Permission-gate the link by `interests.view`. ~1 h.
|
||||
> - **Notes Latest-note teaser missing round / stage context pill** — _src/components/interests/interest-tabs.tsx_ (the "Latest note" block around line 1029-1064) — notes created during a specific stage / EOI round should display a small "Round 2" or stage pill next to the timestamp so reps can see at a glance which phase a note belongs to. Currently shows author + time only. Schema: notes table doesn't carry round info today — would need a derived display from the interest's stage at note creation time (cheapest) or a stamped `created_during_stage` column (more reliable). ~45 min for derived display, ~1.5 h with migration for stamped column. (Same need likely applies to all notes lists, not just the Overview teaser.)
|
||||
> - **InterestBerthStatusBanner: name + link the competing deal** — _src/components/interests/interest-berth-status-banner.tsx_ — the banner that surfaces when a linked berth is under offer to a different active deal currently just says "this berth is under offer elsewhere" without identifying which interest. Reps want a small inline detail: client name + deal stage + a link button to the competing interest, so they can size up the situation (e.g. "this lead won't make it, treat ours as backup"). Service-side: extend the `getInterestBerthStatus()` (or equivalent) response with a `competingInterest: { id, clientName, pipelineStage, ... } | null` field, then surface in the banner. Permission-gate the link by `interests.view`. ~1 h. **SHIPPED in 7ecf4ee:** reuses `/berths/[id]/active-interests` endpoint (already-shipped 292a8b5) instead of extending the original `getInterestBerthStatus()` call. One query per conflicting berth via `useQueries`; picks the `isPrimary` competing interest (falls back to first non-self row); renders inline `<Link>` to the competing detail page. Falls through gracefully when the endpoint hasn't resolved yet.
|
||||
> - **Notes Latest-note teaser missing round / stage context pill** — _src/components/interests/interest-tabs.tsx_ (the "Latest note" block around line 1029-1064) — notes created during a specific stage / EOI round should display a small "Round 2" or stage pill next to the timestamp so reps can see at a glance which phase a note belongs to. Currently shows author + time only. Schema: notes table doesn't carry round info today — would need a derived display from the interest's stage at note creation time (cheapest) or a stamped `created_during_stage` column (more reliable). ~45 min for derived display, ~1.5 h with migration for stamped column. (Same need likely applies to all notes lists, not just the Overview teaser.) **SHIPPED (current-stage variant) in 7ecf4ee:** stage-badge chip next to the timestamp using `STAGE_BADGE` colour map. Shows the deal's CURRENT pipelineStage — historical "stage-at-note-time" lookup would need a per-render `audit_logs` read, over-engineered for a context hint. NotesList rows on other surfaces (full Notes tab) deferred until a real need surfaces.
|
||||
> - **Dimensions columns: add ft↔m toggle in the column header (persisted to user prefs); skip per-row entry-unit indicator** — _src/components/berths/berth-columns.tsx:306_, _src/components/yachts/yacht-columns.tsx:102_, _src/components/clients/client-yachts-tab.tsx:63_, _src/components/companies/company-owned-yachts-tab.tsx:106_ (any current/future Dimensions column), plus _new_ `src/lib/utils/dimensions.ts` for the conversion + format helper, and _src/lib/db/schema/users.ts_ `user_profiles.preferences` for the persisted preference key — five table surfaces render "Dimensions" in feet today; reps used to metric units have to convert in their head.
|
||||
> - **Recommendation on the per-row indicator question:** **column-level toggle alone is enough.** The schema already stores per-dimension entry-unit discriminators (`lengthUnit`, `widthUnit`, `draftUnit` on berths + same pattern on yachts/interests, default `'ft'`) and even keeps separate `_M` numeric columns where metric originals exist (`nominalBoatSizeM`, `waterDepthM`) — so the _data_ knows what was entered. But surfacing that on every row in the table creates visual noise (a small "m" pill next to half the rows) that doesn't help the rep complete a task. The right time to surface entry-unit fidelity is at **EOI / contract / quote generation** time — the merge field renderer should pull the unit + value as entered so the legal document matches the rep's original input verbatim. So: column toggle for UI display, entry-unit honoured in document generation (which already happens for the EOI dialog via `effectiveDimensionUnit`).
|
||||
> - **Implementation:**
|
||||
|
||||
Reference in New Issue
Block a user