feat(launch-readiness-batch): UAT drains, navigation refactor, launch infra, trackers
Bundles the rest of the in-flight work from this UAT round into one
checkpoint. Each sub-area is independent; see the headings below.
UAT polish (drained 11 findings from active-uat.md):
- Dialog primitive default bumped sm:max-w-xl/lg:max-w-3xl →
sm:max-w-2xl/lg:max-w-4xl so multi-field forms + PDF previews
aren't cramped at 1440-1920px.
- Notes tab badge aggregation: new countFor{Client,Yacht,Company}
Aggregated helpers in notes.service mirror the listFor*Aggregated
symmetric-reach joins. yacht-tabs + company-tabs render the
badge; client-tabs already had badge support.
- Supplemental-info form polish bundle: BrandedAuthShell gains a
`width: 'sm' | 'md'` prop (md uses min-h-dvh scroll instead of
fixed inset-0 pin so long forms scroll naturally). Form picks up
port branding (logoUrl + backgroundUrl + appName) via
loadByToken. Address fields completed (street + city + region +
postal + country). Port name eyebrow + success-state copy added.
- new-document-menu Upload-file landing toast: per-file completion
emits toast.success with action link to the destination entity
or folder.
- interest-tabs OverviewTab "from client" pill on Email + Phone
rows via new EditableRow `inheritedFrom` prop.
- create-document-wizard subject picker → segmented button strip
(5 types visible at once).
Launch infra:
- UTM column wiring (Init 1b step 4): migration
0089_website_submissions_utm.sql adds utm_source/medium/campaign/
term/content + composite index (port_id, utm_source, received_at)
for per-campaign rollups. website-inquiries intake accepts the
five fields. Residential intake intentionally untouched per audit
scope.
- Invoicing module gate (Init 1c spike): new
invoices-module.service + invoices layout guard + registry entry
invoices_module_enabled (default false). Audit conclusion in
launch-readiness.md: payments table is canonical money path;
/invoices flow is parallel infrastructure now hidden by default.
Smart-back navigation refactor:
- Replaced breadcrumb component with history-aware Back button.
New route-labels.ts + use-smart-back hook +
navigation-history-tracker so back falls through to the parent
route when there's no prior page in history.
- Sidebar / topbar / mobile-topbar adopt the new pattern; old
breadcrumb-store kept for back-compat consumers but the
breadcrumbs component is gone.
- 6 detail pages (admin/errors per-id + codes, invoices/
upload-receipts, reports kind, tenancies detail, analytics
metric, client detail) migrated.
Trackers + docs:
- docs/launch-readiness.md — master pre-launch tracker. Includes
the reports gap audit (cross-cutting filter set, Marketing +
Financial blockers, custom builder remaining entities, scheduled
CSV/XLSX, template scope picker).
- docs/superpowers/audits/active-uat.md — 15 findings flipped
OPEN → SHIPPED locally with fix-applied notes; 4 OPEN remaining
(each blocked on user input or cross-repo).
- CLAUDE.md — minor session notes carried forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
517
docs/launch-readiness.md
Normal file
517
docs/launch-readiness.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# Launch Readiness — Pre-Prod Initiative
|
||||
|
||||
> **Scope:** the user enumerated five launch-blocking initiatives on
|
||||
> 2026-05-27. This doc is the single home for all of them so we can
|
||||
> track progress without losing items between sessions. Companion to
|
||||
> `docs/superpowers/audits/active-uat.md` (which keeps the live UAT
|
||||
> findings) and `docs/BACKLOG.md` (master backlog index).
|
||||
>
|
||||
> Status tags per item: `OPEN | IN PROGRESS | SHIPPED in <hash> | BLOCKED | DEFERRED`.
|
||||
|
||||
## Initiative 1 — Reports overhaul
|
||||
|
||||
**Status:** IN PROGRESS · Active phase
|
||||
|
||||
Goals (per user, 2026-05-27):
|
||||
|
||||
- Cover all four report categories: **Sales performance**, **Financial**,
|
||||
**Marketing / funnel**, **Operational**.
|
||||
- Template system: load template → modify → re-save OR save as new.
|
||||
- Rich data density: more charts, more graphs, more KPIs.
|
||||
- Output formats: **PDF + CSV + Excel** for each report.
|
||||
- Scheduled reports: cron-driven; auto-email is **optional** (so the
|
||||
admin can schedule a run without forcing an email blast).
|
||||
- Custom builder: full ad-hoc (pick entity, columns, filters, group-by),
|
||||
save as template — but quality-first; we don't ship a janky composer.
|
||||
- UI/UX: stunning, fluid, beautiful. Within the existing white/navy
|
||||
brand language — no off-brand experimental themes.
|
||||
|
||||
Decisions locked (2026-05-27):
|
||||
|
||||
- **Currency**: port branding default
|
||||
- **Rep visibility**: port-scoped admin setting (default depends on
|
||||
team size; PN is single-rep so default = full team)
|
||||
- **AR aging buckets**: standard 30-day (current / 1-30 / 31-60 /
|
||||
61-90 / 90+)
|
||||
- **Custom builder entity scope**: all 10 entities
|
||||
- **Pulse data**: fold into Sales report
|
||||
- **Inquiry-link audit**: yes, audit + fix; no website-repo edits
|
||||
required for the audit itself (link logic is server-side)
|
||||
- **Scope cut for launch**: Sales + Operational ship first as
|
||||
fully-functional reports; Marketing + Financial ship in tandem with
|
||||
their data sources being wired (see Initiatives 2c + 2d below).
|
||||
|
||||
Phases (status snapshot 2026-05-27):
|
||||
|
||||
1. ✅ Foundation + UX overhaul — landing page (within existing
|
||||
design system); charts library audit done; ExcelJS installed.
|
||||
2. ✅ Sales Performance + Operational builders — full report pages
|
||||
with KPIs / charts / tables; client-side Export to CSV + Excel +
|
||||
PDF; server-side PDF endpoint for branded output. _See gaps
|
||||
below._
|
||||
3. ❌ Marketing report — NOT BUILT. Pending Init 1b cutover.
|
||||
4. ❌ Financial report — NOT BUILT. Pending Init 1c decision on
|
||||
whether to enable the invoices module (currently default OFF).
|
||||
5. ⚠️ Custom (ad-hoc) report builder — partial ship.
|
||||
6. ✅ Scheduled reports with optional emailing — BullMQ poll +
|
||||
render path live; recipients optional; PDF-only output.
|
||||
7. ✅ Templates — load / modify / save / save-as / URL deep-link.
|
||||
|
||||
Open considerations carried forward:
|
||||
|
||||
- **Chart library mix.** Project already has `recharts` (simple bar/line/pie)
|
||||
and `echarts` (heatmaps, funnels, complex). Lean on each where it fits;
|
||||
don't add a third unless something specific is missing.
|
||||
- **PDF cover-page treatment.** Each report PDF should open with a
|
||||
branded cover (port logo, title, date range, generated-on stamp). Reuse
|
||||
the existing `branded-document.tsx` shell.
|
||||
|
||||
Working spec: `docs/reports-content-spec.md` (per-category KPIs +
|
||||
charts + tables proposed; updated as we walk through each).
|
||||
|
||||
### Reports — what's left (gap audit 2026-05-27)
|
||||
|
||||
Comparing the working spec against shipped code, here's the bucketed
|
||||
backlog. **Items marked LAUNCH-BLOCK** are needed for the beta cutover;
|
||||
everything else is post-launch polish unless promoted.
|
||||
|
||||
#### Cross-cutting capabilities (apply to every report)
|
||||
|
||||
- ❌ **Period comparison toggle** — "this period vs prior period" delta
|
||||
arrows on KPI cards. Spec calls for it on every report. Not on any.
|
||||
- ❌ **Rep multi-select filter** — exists implicitly via the single-rep
|
||||
leaderboard collapse, but no explicit multi-select dropdown.
|
||||
- ❌ **Source multi-select filter** — Sales has lead-category + outcome
|
||||
filters; the spec also calls for a generic `source` filter (website /
|
||||
referral / broker / manual) on every report.
|
||||
- ❌ **Empty-state copy per report** — currently shows a skeleton; spec
|
||||
wants a "this report needs data first" hint pointing at the right
|
||||
onboarding step.
|
||||
|
||||
#### Phase 2 — Sales report gaps
|
||||
|
||||
- ❌ **Operational-style filter set on Sales** — beyond stage / lead-cat /
|
||||
outcome that shipped, the cross-cutting filters above (period
|
||||
comparison, rep multi-select, source multi-select) are missing.
|
||||
|
||||
#### Phase 2 — Operational report gaps
|
||||
|
||||
- ❌ **Operational-specific filters**: berth area · tenure type ·
|
||||
document type · status filter. None of the four exist. The spec calls
|
||||
these out as drill-down affordances for the heatmap + tables.
|
||||
|
||||
#### Phase 3 — Marketing report (LAUNCH-BLOCK if Marketing is in beta scope)
|
||||
|
||||
Not built. Spec at `docs/reports-content-spec.md` § Report 03 calls for:
|
||||
|
||||
- 6 KPIs (inquiries, inquiry→interest %, inquiry→EOI %, inquiry→won %,
|
||||
top source, avg time-to-respond)
|
||||
- 6 charts (inquiries by source donut, source ROI stacked bar, full
|
||||
funnel, conversion trend, country geo map via `react-simple-maps`,
|
||||
time-to-respond histogram)
|
||||
- 3 tables (top-converting sources, recent inquiries, stuck inquiries)
|
||||
- Filters: specific source, mooring, UTM campaign
|
||||
|
||||
**Blocker:** depends on the website actually sending UTM params (Init
|
||||
1b step 4 — CRM-side shipped, website-side pending) AND on inquiry
|
||||
data flowing from the new intake endpoint (Init 1b step 1 — pending
|
||||
website env flip).
|
||||
|
||||
#### Phase 4 — Financial report (LAUNCH-BLOCK if Financial is in beta scope)
|
||||
|
||||
Not built. Spec at `docs/reports-content-spec.md` § Report 02 calls for:
|
||||
|
||||
- 7 KPIs (revenue collected, pipeline value, deposits, outstanding AR,
|
||||
overdue AR, expenses, net contribution)
|
||||
- 6 charts (revenue by month stacked, quarterly/yearly toggle, EOI →
|
||||
Deposit → Contract funnel, AR aging, cash flow line, expense
|
||||
breakdown donut)
|
||||
- 4 tables (outstanding invoices, recent payments, refund/write-off
|
||||
log, expense ledger)
|
||||
- Filters: invoice kind, payment status, currency, billing entity type
|
||||
|
||||
**Blocker:** depends on the invoices module being in use. Per Init 1c
|
||||
spike, the module is default OFF and the canonical money path is the
|
||||
per-interest Payments tab. **Decision needed**: ship Financial with
|
||||
data from `payments` only (no invoice surface) OR flip invoices module
|
||||
on for PN + train rep + ship Financial. Today the report would be 90%
|
||||
empty.
|
||||
|
||||
#### Phase 5 — Custom builder gaps
|
||||
|
||||
v1 ships 4 entities; full spec wants 10 + advanced composition.
|
||||
|
||||
- ❌ **Missing entities**: yachts, companies, invoices, expenses,
|
||||
documents, websiteSubmissions, payments. Each is a registry-only
|
||||
extension — add a `CustomEntityDefinition` to
|
||||
`src/lib/reports/custom/registry.ts`. ~30 min per entity.
|
||||
- ❌ **Filters beyond date range** — spec wants per-column filter rows
|
||||
(column → operator → value, AND/OR between rows). Today only the
|
||||
date range filter exists.
|
||||
- ❌ **Group by + aggregate** — single group-by dimension + per-column
|
||||
aggregate (count / sum / avg / min / max). Today only a flat list.
|
||||
- ❌ **Column sort** — pick a column + direction. Today rows return
|
||||
with the registry's hardcoded `orderBy`.
|
||||
- ❌ **Live preview as you build** — spec wants debounced re-render on
|
||||
filter / column change. Today the rep clicks "Run query" to fetch.
|
||||
- ❌ **Column whitelist per role** — PII columns (`email`, `phone`)
|
||||
should be gated by `clients.view_pii`. Today all listed columns are
|
||||
available to anyone with `reports.export`.
|
||||
- ❌ **Run-once vs Save-as-template** — the spec asks for three buttons
|
||||
on save (Run once / Save as template / Update existing). Today only
|
||||
the template-save path exists.
|
||||
|
||||
#### Phase 6 — Scheduled runs gaps
|
||||
|
||||
- ❌ **Custom cron strings** — three hardcoded cadences (weekly Mon 9 ·
|
||||
monthly 1st 9 · quarterly 1st 9). Spec implies arbitrary cron.
|
||||
`nextRunFor` in `report-schedules.service.ts` switches on the enum;
|
||||
extend to support a `cron_expression` mode.
|
||||
- ❌ **Scheduled CSV / XLSX** — only PDF is wired through the worker
|
||||
(`renderStandaloneReportRun` in `report-render.service.ts`). For
|
||||
CSV/XLSX, the worker would need to either run the existing client-side
|
||||
exporter server-side (drop ExcelJS into the worker bundle) or build
|
||||
format-specific server renderers.
|
||||
|
||||
#### Phase 7 — Templates gaps
|
||||
|
||||
- ❌ **"Modified ●" indicator** — when the rep changes view state after
|
||||
loading a template, the active-template badge currently just clears.
|
||||
Spec wants a visible "modified" marker so they know they've drifted.
|
||||
- ❌ **Personal vs port-wide scope** — schema has the `visibility`
|
||||
column with `'private' | 'team'` but the UI always saves as port-wide.
|
||||
The Save dialog needs a scope picker.
|
||||
- ❌ **"Owned by" attribution** — templates with `visibility='team'`
|
||||
should show creator name. Schema captures `createdBy`; UI doesn't
|
||||
surface it.
|
||||
- ❌ **Promote-to-port-wide affordance** — once shipped, a "Share with
|
||||
team" action on personal templates that flips visibility.
|
||||
|
||||
#### Net launch-readiness for reports
|
||||
|
||||
If the launch scope is **Sales + Operational only**, reports are
|
||||
launch-ready with the polish items above as post-launch follow-ups.
|
||||
|
||||
If the launch scope includes **Marketing + Financial**, both reports
|
||||
need to be built AND their data plumbing finished (Init 1b website
|
||||
flip + UTM forwarding for Marketing; invoices module + rep training
|
||||
for Financial).
|
||||
|
||||
The cross-cutting filter set (period comparison, rep / source
|
||||
multi-select, empty-state copy) is the highest-value polish that's
|
||||
visible on every report — call it ~6-8 hours of work spread across
|
||||
both shipped report pages + the shared FilterBar component.
|
||||
|
||||
---
|
||||
|
||||
## Initiative 1b — Marketing data pipeline cutover
|
||||
|
||||
**Status:** OPEN · Blocks the Marketing report
|
||||
|
||||
The CRM has the **full infrastructure** for marketing intake +
|
||||
attribution; it's just not connected end-to-end.
|
||||
|
||||
What's built:
|
||||
|
||||
- **Email-open pixel tracking**: `src/app/api/public/email-pixel/[sendId]/route.ts`
|
||||
- `src/lib/email/tracking-pixel.ts`. Sales sends with
|
||||
`trackOpens=true` get a 1×1 pixel; opens record to
|
||||
`email_send_opens` and cross-post to Umami.
|
||||
- **Umami integration**: `@umami/node` installed; `src/lib/services/umami.service.ts`
|
||||
is the wrapper. Outcome events (EOI sent, deposit received, etc.)
|
||||
already cross-post into Umami.
|
||||
- **Website inquiry intake endpoint**: `/api/public/website-inquiries`
|
||||
in the CRM, paired with `/api/public/residential-inquiries`. Both
|
||||
validate + dual-write into `website_submissions`.
|
||||
- **Website posting code**: `Port Nimara/Website/server/utils/crmIntake.ts:72`
|
||||
has the matching POST. Just needs the env var to point at the new
|
||||
CRM.
|
||||
|
||||
What's NOT connected yet:
|
||||
|
||||
1. **Website env `CRM_INTAKE_URL`** still points at the old portal (or
|
||||
isn't set). Flipping this is a ~5-min config change inside the
|
||||
website Nuxt deploy. After flip, every website inquiry lands in
|
||||
`website_submissions` + auto-routes to the inquiry-triage queue.
|
||||
2. **Backfill of historical inquiries** from the old portal so the
|
||||
Marketing report has launch-day history rather than starting from
|
||||
zero. Reads from `client_portal_v2`'s inquiry table, inserts into
|
||||
`website_submissions` with original `receivedAt` timestamps,
|
||||
re-links to existing CRM clients via dedup (email/phone).
|
||||
3. **Umami funnel events on the marketing site itself**. The Umami
|
||||
project exists; what's unclear is whether the marketing site is
|
||||
firing `event:` calls on key actions (form submitted, brochure
|
||||
downloaded, virtual-tour started). Audit needed.
|
||||
4. **UTM column wiring**. ✅ CRM-side SHIPPED — migration `0089_website_submissions_utm.sql`
|
||||
adds `utm_source / utm_medium / utm_campaign / utm_term / utm_content`
|
||||
to `website_submissions` plus a `(port_id, utm_source, received_at)`
|
||||
composite index for per-campaign rollups. `/api/public/website-inquiries`
|
||||
accepts the five fields in the request body and persists them on
|
||||
insert. **Pending website-side change**: the marketing site's
|
||||
`crmIntake.ts` POST must forward UTM params from the form's query
|
||||
string / cookies. **Pending residential parity**: residential
|
||||
inquiries (`/api/public/residential-inquiries`) don't go through
|
||||
`website_submissions`; if Marketing report needs UTM attribution on
|
||||
residential leads too, add the same columns to `residential_clients`
|
||||
in a follow-up.
|
||||
|
||||
Sequencing:
|
||||
|
||||
- Step 1 is the cutover unblock (do during launch window itself).
|
||||
- Step 2 is part of Initiative 5 (data migration).
|
||||
- Step 3 is a website-side audit (Initiative 3).
|
||||
- Step 4 is a small CRM-side schema add (one migration + 4 column
|
||||
reads). Decision pending: ship at launch or defer to Phase 2.
|
||||
|
||||
---
|
||||
|
||||
## Initiative 1c — Invoicing audit-and-finish
|
||||
|
||||
**Status:** SPIKE COMPLETE · Module-toggle shipped · Financial report deferred
|
||||
|
||||
### Audit findings (2026-05-27 spike)
|
||||
|
||||
The CRM has two parallel money-receiving flows in active code:
|
||||
|
||||
1. **`payments` table — canonical, in active use.** Schema comment at
|
||||
`src/lib/db/schema/pipeline.ts:75` is unambiguous: "The CRM does
|
||||
NOT generate invoices — clients pay banks directly. We record that
|
||||
money was received." Linked to `interests`. `recordPayment`
|
||||
auto-advances pipeline to `deposit_paid` when the cumulative
|
||||
deposit total hits `depositExpectedAmount`. This is the surface
|
||||
reps actually use; payments are recorded from the per-interest
|
||||
**Payments** tab.
|
||||
2. **`invoices` + `invoice_line_items` table — orphaned in the UI.**
|
||||
Full builder (line items, PDF, send, mark-paid) exists at
|
||||
`/[portSlug]/invoices/new`. The sidebar nav entry was removed
|
||||
earlier; only the page itself can link to `invoices/new`. Dev DB
|
||||
has zero rows. The standalone surface is parallel infrastructure
|
||||
for the rare case where an operator wants to invoice a client
|
||||
directly from the CRM, plus the employee-expense-report flow
|
||||
(`expenses → invoices` PDF).
|
||||
|
||||
### Decision (per the existing "intentionally manual elsewhere" branch)
|
||||
|
||||
Ship a port-level module toggle, default OFF, identical pattern to
|
||||
the Tenancies and Expenses toggles. The Financial report stays
|
||||
deferred from launch since the canonical Payments tab feeds the
|
||||
Sales report (which is shipping) — separate Financial dashboard adds
|
||||
no value when there's no second money-receiving flow.
|
||||
|
||||
**What shipped (2026-05-27):**
|
||||
|
||||
- `system_settings` registry entry `invoices_module_enabled` (boolean,
|
||||
port-scoped, default `false`) — added to
|
||||
`src/lib/settings/registry.ts`.
|
||||
- New module-gate service `src/lib/services/invoices-module.service.ts`
|
||||
with `isInvoicesModuleEnabled(portId)` (same shape as
|
||||
`isExpensesModuleEnabled`).
|
||||
- Layout-level guard at `src/app/(dashboard)/[portSlug]/invoices/layout.tsx`
|
||||
— every `/invoices/*` route renders `<ModuleDisabledPage>` when the
|
||||
port hasn't opted in. Admins can flip on from Admin → Settings;
|
||||
historical rows preserved.
|
||||
|
||||
**What's NOT changed:**
|
||||
|
||||
- API endpoints (`/api/v1/invoices/*`) still respond — historical PDF
|
||||
links + send-flow webhooks keep resolving regardless of the toggle.
|
||||
- The `payments` flow is untouched and continues to be the canonical
|
||||
money-received path.
|
||||
- The expense → invoice flow (employee expense reports) is
|
||||
unaffected since employee-expense PDFs flow through a different
|
||||
surface (`/expenses`) that lives behind its own module gate.
|
||||
|
||||
**Follow-up:** if the user later wants per-port branded
|
||||
client-facing invoicing from inside the CRM, the surface is ready to
|
||||
turn on with no schema work — just flip `invoices_module_enabled = true`.
|
||||
|
||||
---
|
||||
|
||||
## Initiative 2 — Multi-agent codebase audit
|
||||
|
||||
**Status:** OPEN · Awaiting kickoff
|
||||
|
||||
User ask: "deep, multi-agent audit of all routes, naming, text, UX, and
|
||||
… dig through the entire code of everything in the system (especially
|
||||
related to the sales process) and find any issues in the logic or how
|
||||
the functionality interacts with each other, how data is shared and
|
||||
persists where needed. Also a deep security audit."
|
||||
|
||||
Audit dimensions (use one specialised agent per dimension, in parallel):
|
||||
|
||||
| # | Dimension | Specialised agent | Output |
|
||||
| --- | ----------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 1 | **Sales pipeline logic** | `feature-dev:code-explorer` | Trace every stage transition; verify auto-advance rules, EOI gating, deposit handling, contract signing. Look for stale enum references (the 9→7 stage migration left some bugs). |
|
||||
| 2 | **Cross-entity data flow** | `feature-dev:code-explorer` | Map polymorphic ownership (yacht/company), interest_berths (multi-berth), document folders (aggregated projection), notes (4-table dispatch). Find divergence between docs and code. |
|
||||
| 3 | **Security** | `security-review` (existing skill) | OWASP API Top 10, auth bypass, IDOR, injection, secret leakage, GDPR exposure. Multi-tenant boundary checks (port_id at every join). |
|
||||
| 4 | **API surface consistency** | `code-review:code-review` | `{ data: T }` envelope adherence, `errorResponse(error)` usage, `parseBody(req, schema)` usage, 204 vs JSON, withAuth+withPermission composition. |
|
||||
| 5 | **UI/UX consistency** | `frontend-design:frontend-design` review | Visual inconsistencies, copy/text issues, accessibility, mobile parity, brand drift, em-dashes, generic SaaS slop. |
|
||||
| 6 | **Schema vs code divergence** | `feature-dev:code-explorer` | Migrations vs Drizzle schema files vs service helpers — find any column the DB has that no service touches, or any service field with no migration. |
|
||||
| 7 | **Documenso integration** | `feature-dev:code-explorer` | Full v1↔v2 path coverage, webhook idempotency, template field mapping, EOI generation (both pathways), error recovery. |
|
||||
| 8 | **Storage & file lifecycle** | `feature-dev:code-explorer` | S3↔filesystem switching, file orphans, signed-URL expiry, GDPR export coverage, magic-byte validation everywhere. |
|
||||
|
||||
Coordination:
|
||||
|
||||
- Use a **single coordinator session** that fans out via `Agent` /
|
||||
`TaskCreate` with `subagent_type` set per dimension. Each agent writes
|
||||
findings to a per-dimension scratch file under
|
||||
`docs/audits/2026-05-27/<dimension>.md`, then the coordinator
|
||||
consolidates into a single triage doc with severity tags.
|
||||
- Pass `model: "opus"` on every agent spawn — Sonnet/Haiku context
|
||||
windows compact too fast under MCP baseline (per memory
|
||||
`feedback_subagent_context_bloat`).
|
||||
|
||||
Output: `docs/audits/2026-05-27/findings-master.md` with per-finding
|
||||
severity (`CRITICAL | HIGH | MED | LOW`), file:line refs, and
|
||||
recommended fix. Critical + High get fixed before launch.
|
||||
|
||||
---
|
||||
|
||||
## Initiative 3 — Marketing website integration
|
||||
|
||||
**Status:** OPEN · Needs scope clarification
|
||||
|
||||
User ask: "make our relevant edits to the marketing website to prepare
|
||||
for the deployment and integration of our new system."
|
||||
|
||||
The marketing site lives in `/Users/matt/Repos/Port Nimara/Website`
|
||||
(separate Nuxt repo). Integration touch points the CRM exposes today:
|
||||
|
||||
- **`/api/public/berths`** + **`/api/public/berths/[mooringNumber]`** —
|
||||
feeds the marketing site's berth list / detail. Status precedence
|
||||
Sold > Under Offer > Available is already wired.
|
||||
- **`/api/public/health`** — dual-mode health check; the website should
|
||||
call the authenticated variant (with `WEBSITE_INTAKE_SECRET`) on
|
||||
startup so it refuses to start when pointed at the wrong CRM env.
|
||||
- **`/api/public/website-inquiries`** — intake endpoint for the contact
|
||||
form; dual-writes inquiry into the CRM.
|
||||
- **Inquiry email ownership** — at cutover, inquiry emails move from
|
||||
the website to the CRM (per memory
|
||||
`project_email_ownership_at_cutover`). Templates + settings keys
|
||||
already exist; berth public endpoint + admin recipient UI still
|
||||
needed (per existing memory).
|
||||
- **Cover photography + branding assets** — the new system uses
|
||||
`branding_email_background_url` etc.; ensure the website assets
|
||||
match.
|
||||
|
||||
Open work (needs user input on priority):
|
||||
|
||||
- Wire the website's contact form to `/api/public/website-inquiries`
|
||||
with the new payload shape.
|
||||
- Add the `WEBSITE_INTAKE_SECRET` to the website's env, point at the
|
||||
authenticated `/api/public/health`.
|
||||
- Update berth-detail page to consume the new `/api/public/berths/...`
|
||||
shape (the JSON mirrors the legacy NocoDB shape so this should be
|
||||
a no-op — VERIFY).
|
||||
- Replace any hard-coded "noreply@portnimara.com" sender on the
|
||||
website side with the CRM-controlled From address (so per-port
|
||||
branding wins).
|
||||
- Confirm the website's caching headers don't fight ours
|
||||
(`s-maxage=300, stale-while-revalidate=60` on berth endpoints).
|
||||
|
||||
---
|
||||
|
||||
## Initiative 4 — End-to-end testing
|
||||
|
||||
**Status:** OPEN · Needs scope clarification
|
||||
|
||||
User ask: "end to end testing of all sales functions, generating
|
||||
EOIs/documents (especially), ensuring all UX/UI is fluid, beautiful,
|
||||
relevant and helps the user go through the sales process effortlessly."
|
||||
|
||||
Existing infrastructure (per `CLAUDE.md`):
|
||||
|
||||
- `tests/e2e/smoke` — fast click-through (~10 min, ~125 specs)
|
||||
- `tests/e2e/exhaustive` — deeper UI coverage
|
||||
- `tests/e2e/destructive` — archive/delete/cancel paths
|
||||
- `tests/e2e/realapi` — opt-in real Documenso + IMAP round-trip
|
||||
- `tests/e2e/visual` — pixel-diff baselines
|
||||
|
||||
Pre-launch test gaps to fill (proposed):
|
||||
|
||||
1. **End-to-end sales journey** (single Playwright spec, real-API): new
|
||||
inquiry → qualified → EOI generated (Documenso) → client signs →
|
||||
developer countersigns → reservation → deposit recorded → contract
|
||||
generated → contract signed → tenancy auto-created → berth marked
|
||||
sold. Assert every stage transition + every email fires.
|
||||
2. **EOI generation parity** between both pathways (in-app
|
||||
`fill-eoi-form` vs Documenso template). Same `EoiContext` should
|
||||
produce equivalent PDFs.
|
||||
3. **Multi-berth EOI rendering** — berth range formatter assertion
|
||||
(`A1-A3, B5-B7` from `interest_berths`).
|
||||
4. **Documenso webhook idempotency** — replay the same `DOCUMENT_COMPLETED`
|
||||
webhook three times; assert single `files.folder_id` write + no
|
||||
duplicate audit-log rows.
|
||||
5. **Storage backend swap** — switch port to filesystem, generate EOI,
|
||||
verify file lands; switch back to S3, confirm migrate script moves
|
||||
the blob correctly.
|
||||
6. **Visual snapshot refresh** for the new Reports UI + back-button
|
||||
smart-back changes (this conversation).
|
||||
7. **Mobile parity** for the entire sales journey (different Playwright
|
||||
project or `--config` variant).
|
||||
|
||||
Each gap above becomes one or two new spec files. Coordinate with
|
||||
Initiative 2's audit so we don't double-test.
|
||||
|
||||
---
|
||||
|
||||
## Initiative 5 — Data migration (legacy → new)
|
||||
|
||||
**Status:** OPEN · High effort · Likely blocker for cutover
|
||||
|
||||
User ask: "start pulling all existing prod data from the old system and
|
||||
connected systems (we'll have to backfill the EOIs by pulling them
|
||||
through MinIO — it's a fucking mess so I'll really need your help
|
||||
automating/speeding up that process) and initiate a preliminary switch
|
||||
over."
|
||||
|
||||
Sources to drain:
|
||||
|
||||
| Source | Storage | Entities | Notes |
|
||||
| ------------------------------- | ------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| Old NocoDB tables | Postgres / NocoDB | Clients, yachts, companies, interests, berths, EOIs (metadata) | Already imported in earlier migration; verify currency vs prod NocoDB. |
|
||||
| Old portal (`client_portal_v2`) | Nuxt + Postgres | Portal users, signing history, sent invitations | Need to confirm what hasn't been migrated yet. |
|
||||
| MinIO (legacy bucket) | Object storage | EOI PDFs (signed + unsigned), receipts, contracts | The "fucking mess" — naming is inconsistent, organisation unclear, need to map each blob back to its CRM entity. |
|
||||
| Documenso v1 (live) | Documenso server | In-flight signing envelopes + signed PDFs | Migration question: do we cut new EOIs to v2 and let v1 envelopes finish, or migrate the in-flight? |
|
||||
| Email archives | IMAP / mail server | Inquiry replies, signing reminders, deposit confirmations | Probably out of scope for cutover (read-only history). |
|
||||
|
||||
Migration script plan (write under `scripts/migration/`):
|
||||
|
||||
1. **`probe-minio.ts`** — scan the legacy MinIO bucket, list every blob,
|
||||
try to extract a client / interest / berth identifier from filename
|
||||
patterns. Produce `docs/migration/minio-blob-inventory.csv` with
|
||||
`key, size_bytes, mime, probable_entity_type, probable_entity_id, confidence`.
|
||||
2. **`backfill-eoi-pdfs.ts`** — for each inventoried blob with confidence
|
||||
≥ HIGH, copy from legacy MinIO into the new storage backend, create a
|
||||
matching `files` row + `documents` row, deposit into the right
|
||||
entity folder via the existing `ensureEntityFolder` helper. Idempotent
|
||||
via `legacy_minio_key` column (add via migration if missing).
|
||||
3. **`reconcile-nocodb.ts`** — diff the live NocoDB tables against our
|
||||
imported state; report rows added/changed/deleted since last import.
|
||||
4. **`preflight-cutover.sh`** — orchestrator script that runs the three
|
||||
above in order, writes a final report.
|
||||
|
||||
Cutover plan:
|
||||
|
||||
1. Freeze writes on the old system (NocoDB read-only, portal
|
||||
maintenance page).
|
||||
2. Run `preflight-cutover.sh` against frozen sources.
|
||||
3. Manual reconciliation of probe-minio rows where confidence < HIGH
|
||||
(likely a few hundred blobs — the user explicitly flagged this is
|
||||
manual labour, automation helps but doesn't replace it).
|
||||
4. DNS / website pointer flip.
|
||||
5. Watch error_events for 24h; rollback plan = re-enable old system
|
||||
writes and stop the cutover commit.
|
||||
|
||||
---
|
||||
|
||||
## Cross-initiative open questions
|
||||
|
||||
- **When to wrap the launch audit doc.** I'd suggest: after Initiative
|
||||
2's findings are triaged AND Initiatives 3-5 reach IN PROGRESS. At
|
||||
that point this file becomes the launch-day-runbook.
|
||||
- **Who's the launch sponsor / decision-maker?** Different from
|
||||
"user / matt"? Affects who signs off on cutover.
|
||||
- **Soft launch vs hard cutover?** Hard cutover is simpler operationally
|
||||
but risky; soft launch (parallel writes for a week) is safer but
|
||||
requires the old system to keep accepting writes for longer.
|
||||
@@ -36,7 +36,8 @@
|
||||
|
||||
### Dialog primitive default too narrow → bump platform-wide
|
||||
|
||||
- **`OPEN`** — _src/components/ui/dialog.tsx_ (DialogContent base default).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/ui/dialog.tsx_ (DialogContent base default).
|
||||
- **Fix applied:** default bumped from `sm:max-w-xl lg:max-w-3xl` to `sm:max-w-2xl lg:max-w-4xl`. Confirm dialogs override DOWN with `sm:max-w-md`; PDF preview / signing dialogs override UP with `lg:max-w-5xl` or `lg:max-w-[min(95vw,1400px)]`.
|
||||
- **Symptom:** Dialog primitive's default is `sm:max-w-lg` (512px), which is far too narrow for most content (forms, file previews, signing details). Even the earlier per-dialog `lg:max-w-4xl` bump only fixed the dialogs I explicitly migrated; everything still using the default — including FilePreviewDialog (which overrides to `max-w-4xl` but PDFs are unreadable at that width) — stays cramped on desktop.
|
||||
- **Fix:** bump the Dialog primitive base to `sm:max-w-2xl lg:max-w-4xl` so every Dialog gets a sane wide-screen default. Per-dialog overrides ride on top for cases that need wider (PDF preview) or narrower (confirm dialogs).
|
||||
|
||||
@@ -272,7 +273,8 @@
|
||||
|
||||
### Recommender tier contradicts berth status
|
||||
|
||||
- **`OPEN`** — _src/lib/services/berth-recommender.service.ts:223_ (`classifyTier`) + _src/components/interests/berth-recommender-panel.tsx_ (card render).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/lib/services/berth-recommender.service.ts:223_ (`classifyTier`) + _src/components/interests/berth-recommender-panel.tsx_ (card render).
|
||||
- **Fix applied:** `TierInputs.status` propagated end-to-end. `classifyTier` now collapses the contradiction: `status='sold'` → D, `status='under_offer'` (with or without active interest rows) → C, otherwise existing rules. `RawRow.status` already feeds in via `classifyTier(r)`.
|
||||
- **Symptom:** berth D39 shows both `Under Offer` (status pill) AND `Open` (recommender tier). The tooltip definition contradicts itself: "Open: never had an interest, ready for new prospects."
|
||||
- **Root cause:** `classifyTier` only reads from `interest_berths` aggregates (active count / lost count / max active stage). A berth whose `berths.status` column says `Under Offer` — set manually by an admin, imported from NocoDB, or left over from a stale row — has zero entries in interest_berths if no active interest is currently driving the status, so the tier classifier returns A (Open). The two signals come from different sources and aren't reconciled.
|
||||
- **Fix:** add `berthStatus` to `TierInputs` and bias `classifyTier`:
|
||||
@@ -284,7 +286,8 @@
|
||||
|
||||
### Berth occupancy info — surface competing interest on every non-available status
|
||||
|
||||
- **`OPEN`** — _src/components/interests/linked-berths-list.tsx_ LinkedBerthRowItem + _src/components/interests/berth-recommender-panel.tsx_ (recommendation cards at ~line 184) + _src/components/interests/interest-berth-status-banner.tsx_ (deal-level banner — already shipped).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/berths/berth-occupancy-chip.tsx_ (shared chip) + adopted in _linked-berths-list_ (LinkedBerthRowItem) + _berth-recommender-panel_ (recommendation cards) + _interest-berth-status-banner_ (deal-level banner).
|
||||
- **Fix applied:** new `<BerthOccupancyChip berthId excludeInterestId={currentInterestId} />` reuses `/api/v1/berths/[id]/active-interests`. Renders inline on every non-available status surface (linked-berths list, recommender cards, deal banner). Hides when the only competing interest is the current one.
|
||||
- **React-grab anchors:** `<span>Under Offer</span>` in StatusPill in LinkedBerthRowItem; same pill in the recommender card body.
|
||||
- **Symptom:** anywhere a berth's status renders as "Under Offer" / "Sold" / "Reserved" the rep currently has no idea WHO is responsible for that status. They have to navigate to the berth detail page (or guess) to find the competing interest or the closed-deal client.
|
||||
- **Fix:** reuse the existing `/api/v1/berths/[id]/active-interests` endpoint (shipped for the columns popover + `InterestBerthStatusBanner`) and surface the top competing interest inline on every non-available status surface. Show client name + stage pill + a link to the competing interest detail. Hide when the only competing interest is the current one (self-conflict makes no sense to flag).
|
||||
@@ -303,9 +306,8 @@
|
||||
|
||||
### Notes tab header count doesn't aggregate
|
||||
|
||||
- **`OPEN`** — _src/lib/services/clients.service.ts:441-444_ (clientNotes count) + similar in yachts.service / companies.service.
|
||||
- **Symptom:** rep adds a note to the client's linked yacht; client detail's Notes tab badge still reads "0" because the count only includes direct `client_notes` rows. The NotesList itself shows the aggregated yacht / company / interest notes correctly — the tab badge is the only piece that's not aggregated.
|
||||
- **Fix:** extend the count to mirror the aggregator's symmetric-reach. For a client: `count(client_notes WHERE client_id=X) + count(yacht_notes WHERE current_owner_type='client' AND current_owner_id=X) + count(company_notes joined via company_memberships) + count(interest_notes joined via interests.client_id)`. Mirror for yacht / company tab counts. ~1–2h with vitest for the join logic.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/lib/services/notes.service.ts_ (new `countFor{Client,Yacht,Company}Aggregated`) + _clients.service.ts_, _yachts.service.ts_, _companies.service.ts_ (wired into `getById` responses) + _yacht-tabs.tsx_, _company-tabs.tsx_ (badge prop).
|
||||
- **Fix applied:** new symmetric-reach count helpers in `notes.service.ts` mirror the existing `listFor*Aggregated` joins. Client tab counts client + interest + yacht (owned) + company (active membership) notes; yacht tab counts yacht + polymorphic-owner + linked-interest notes; company tab counts company + owned-yacht + their-interest notes. `getYachtById` / `getCompanyById` now return `noteCount`; tab definitions render the badge.
|
||||
|
||||
### Admin toggle to disable Tenancies entirely
|
||||
|
||||
@@ -361,21 +363,21 @@
|
||||
|
||||
### Tag chips missing wherever StageStepper renders
|
||||
|
||||
- **`OPEN`** — _src/components/clients/client-pipeline-summary.tsx_ (StageStepper component + ClientInterestRow type + useClientInterests query) + _src/components/clients/client-interests-tab.tsx_ (InterestRowItem) + every other call site that renders `<StageStepper>`.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/clients/client-pipeline-summary.tsx_ + _src/components/clients/client-interests-tab.tsx_.
|
||||
- **Fix applied:** every StageStepper call site (Overview top-deal block, Overview interest list, Interests-tab row item, Interests-tab detail panel) renders a tag-chip strip under the stepper. ClientInterestRow type carries `tags?: Array<{ id, name, color }>` and the interests list endpoint resolves the join in a single batch.
|
||||
- **React-grab anchor:** `<div class="flex-1 truncate...">Qual.</div>` in StageStepper in InterestRowItem in ClientInterestsTab.
|
||||
- **Symptom:** the InterestRowItem cards show berth label + stage badge + stepper, but no tag chips. Tags are first-class on interests everywhere else (detail page, list view) — the same chips should follow the StageStepper everywhere it appears so reps see "Hot lead / VIP / Returning client" context at a glance without drilling in.
|
||||
- **Fix:** (a) extend `ClientInterestRow` with `tags?: Array<{ id, name, color }>` and surface from `useClientInterests` (`/api/v1/interests?clientId=X`). (b) Render a small tag-chip strip just above or below the StageStepper in InterestRowItem + every other StageStepper call site (currently `client-interests-tab.tsx:88, 263`, `client-pipeline-summary.tsx:224, 340`). (c) Cap to ~3 chips with a "+N" overflow indicator so long tag lists don't blow up the row height.
|
||||
|
||||
### New-document "Upload file" — unclear where the file lands
|
||||
|
||||
- **`OPEN`** — _src/components/documents/new-document-menu.tsx_ (the "Upload file" action) + _src/components/files/file-upload-zone.tsx_ (no post-upload navigation cue).
|
||||
- **React-grab anchor:** `<button>...New document</button>` in DocumentsHub PageHeader.
|
||||
- **Symptom:** clicking "Upload file" opens a dialog whose description says "File will be added to the current folder" — but doesn't name the folder. After upload, the dialog closes silently; the file appears somewhere but the rep has no toast / navigation cue confirming it landed. If they uploaded from the hub root with a different folder selected in the sidebar, they may not realize the file went to the selected folder rather than the root.
|
||||
- **Fix:** (a) Dialog description names the destination folder explicitly ("File will be added to **Clients / Matthew Ciaccio / Deal A1-A3**" with breadcrumb chain). (b) Post-upload toast: "Uploaded `<filename>` → \[folder breadcrumb\]" with a "View folder" action that selects that folder in the hub. (c) Optionally auto-navigate to the destination folder on success when the upload originated from a different folder (defer this — toast may be enough).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/documents/new-document-menu.tsx_ (Upload file dialog's `onUploadComplete`).
|
||||
- **Fix applied:** per-file completion now emits a `toast.success('Uploaded <filename>')` with an action link. When the upload happened under an entity (clients/companies/yachts) the action navigates to that entity's detail page; otherwise it opens the destination folder via `/documents?folderId=…`. Still deferred (lower priority): naming the destination folder verbatim in the pre-upload dialog description.
|
||||
|
||||
### Recent files — no link to folder or attached entity
|
||||
|
||||
- **`OPEN`** — _src/components/documents/documents-hub.tsx_ HubRootView (the "Recent files" panel) + _src/lib/services/files.service.ts_ (list response shape).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/documents/hub-root-view.tsx_ + _src/lib/services/files.ts_ (`listFiles`).
|
||||
- **Fix applied:** each row in the Recent files panel shows a folder chip (linking to `/documents?folderId=…`) and an entity badge (Interest / Client / Yacht / Company → entity detail page). `listFiles` already resolves `folderName / clientName / yachtName / companyName / interestSummary` in a single batched lookup so no N+1 cost.
|
||||
- **React-grab anchor:** `<h3 class="flex items-cent...">Recent files</h3>` in HubRootView.
|
||||
- **Symptom:** each recent-file row only shows filename + size + date; the rep has to remember which client / interest the file belongs to. No CTA to jump into the parent folder either.
|
||||
- **Fix:** extend row payload with `{ folderId, folderName, clientId, clientName, interestId, interestBerthLabel }`. Render a small badge column showing the attached entity (client name or interest's berth label, like the EntityFolderView pattern already shipped). Right-hand action gains an icon button "Open folder" that navigates to the folder view in Documents Hub.
|
||||
@@ -386,27 +388,23 @@
|
||||
|
||||
### Supplemental-info form — no port branding, no logo on top
|
||||
|
||||
- **`OPEN`** — _src/app/public/supplemental-info/[token]/page.tsx_ + _src/components/shared/branded-auth-shell.tsx_ + _src/lib/services/supplemental-forms.service.ts_ (loadByToken).
|
||||
- **Symptom:** opening a token link lands on the form with no logo / branding — the page sits OUTSIDE the `(portal)` route group (relocated 2026-05-21 to dodge the portal kill-switch) and no `<AuthBrandingProvider>` wraps it, so `BrandedAuthShell` falls back to neutral.
|
||||
- **Fix:** extend `loadByToken` to also return `branding: { logoUrl, backgroundUrl, appName }` resolved via `getPortBrandingConfig(token.portId)`; the API surfaces it; the page passes it to `BrandedAuthShell` via the explicit `branding` prop.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/app/public/supplemental-info/[token]/page.tsx_ + _src/components/shared/branded-auth-shell.tsx_ + _src/lib/services/supplemental-forms.service.ts_ (loadByToken).
|
||||
- **Fix applied:** `loadByToken` now returns `port: { name, logoUrl, backgroundUrl }` via `getPortBrandingConfig(token.portId)`. Page passes that directly to `BrandedAuthShell` via the explicit `branding` prop so the logo + backdrop render regardless of the route-group context.
|
||||
|
||||
### Supplemental-info form — extends edge-to-edge on long forms
|
||||
|
||||
- **`OPEN`** — _src/components/shared/branded-auth-shell.tsx_.
|
||||
- **Symptom:** the form's card is taller than the viewport, but the shell uses `fixed inset-0 flex items-center` (right for short auth surfaces), so the form content butts against the top/bottom of the viewport with no breathing room.
|
||||
- **Fix:** switch this page to a scrollable layout — natural page flow with padding above/below the card, logo at the top, footer note at the bottom. Don't touch the existing auth surfaces (login / reset-password / set-password / activate); they keep the `fixed inset-0` shell.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/shared/branded-auth-shell.tsx_.
|
||||
- **Fix applied:** added a `width?: 'sm' | 'md'` prop. `'md'` widens the card to `max-w-xl` and swaps the `fixed inset-0` viewport pin for a normal `min-h-dvh` page scroll, so a 20+ field form scrolls naturally on mobile instead of clipping under the rubber-band cap. Login surfaces stay on `'sm'` (default) with the original pinned-and-centered shell.
|
||||
|
||||
### Supplemental-info form — address fields incomplete
|
||||
|
||||
- **`OPEN`** — _src/app/public/supplemental-info/[token]/page.tsx_ + _src/app/api/public/supplemental-info/[token]/route.ts_ + _src/lib/services/supplemental-forms.service.ts_.
|
||||
- **Symptom:** form has a single "Address" textarea + Country combobox. The CRM's `client_addresses` table holds: street, city, subdivisionIso (region/state), postalCode, countryIso. The form drops most of those.
|
||||
- **Fix:** match the CRM shape — separate inputs for street + city + subdivision (SubdivisionCombobox) + postal code + country. Plumb through prefill API + submission validator + applySubmission diff/persist.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/app/public/supplemental-info/[token]/page.tsx_ + _src/app/api/public/supplemental-info/[token]/route.ts_ + _src/lib/services/supplemental-forms.service.ts_.
|
||||
- **Fix applied:** form now exposes street + city + region/state + postal code + country as separate inputs, mirroring the `client_addresses` shape. `loadByToken` returns the existing values for prefill; the API schema accepts the new fields; `applySubmission` diffs + writes them per-column with field-history entries.
|
||||
|
||||
### Supplemental-info form — no context about where details land
|
||||
|
||||
- **`OPEN`** — _src/app/public/supplemental-info/[token]/page.tsx_.
|
||||
- **Symptom:** form header says "A few details before we draft your EOI" but doesn't surface that the exact values entered will appear on the legal document. Setting that expectation up-front reduces support questions about why we're asking.
|
||||
- **Fix:** add a small info banner ("These details will appear on your Expression of Interest document") near the top, soft tone, doesn't disrupt the flow.
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/app/public/supplemental-info/[token]/page.tsx_.
|
||||
- **Fix applied:** added the port name as an eyebrow above the title ("PORT NIMARA") and a clarifying line in the intro: "Submissions go straight to the team handling your application." The success state also references the port name explicitly.
|
||||
|
||||
### Marketing-site form parity — primary surface lives on the website
|
||||
|
||||
@@ -416,7 +414,8 @@
|
||||
|
||||
### Interest OverviewTab — inherit empty fields from client + visually denote
|
||||
|
||||
- **`OPEN`** — _src/components/interests/interest-tabs.tsx_ (OverviewTab) + _src/lib/services/interests.service.ts_ (response payload).
|
||||
- **`SHIPPED locally (not yet committed)`** — _src/components/interests/interest-tabs.tsx_ (OverviewTab + EditableRow).
|
||||
- **Fix applied:** EditableRow gains an `inheritedFrom?: 'client' | 'yacht' | 'company'` prop that renders a small "from client" / "from yacht" pill next to the label. Wired on the Email + Phone rows so reps know edits propagate to the client-level contacts table. Yacht-dimension inheritance was already in place via the `yachtDimensions` payload + per-axis "from yacht" pill in the desired-dimensions block; both inheritance signals now use the same visual language.
|
||||
- **React-grab anchor:** `<div class="space-y-1" />` in OverviewTab, inside the TabsContent presence wrapper.
|
||||
- **Symptom:** the OverviewTab shows interest-level fields that, when empty, render as " - ". If the client (or linked yacht for dimensions) already has those details on file, the rep has to navigate to the client / yacht to see them. Adds friction + risks reps re-asking the client.
|
||||
- **Fix:** when an interest field is null but the client/yacht has it filled, render the inherited value with a small visual cue (e.g. italic + a "from client" or "from yacht" pill). Editing in place should write to the interest's own column (override). Specific candidates:
|
||||
@@ -494,7 +493,8 @@
|
||||
|
||||
### Automate signing — single button that cascades invites + emails the completed doc (REFINED with signing-order awareness)
|
||||
|
||||
- **`OPEN`** — feature request, no implementation. Refined below to handle both sequential AND concurrent modes properly.
|
||||
- **`SHIPPED locally (not yet committed)`** — committed earlier this UAT round as commit `fe5f98d` (`feat(automate-signing): one-click invitation kickoff + auto cascade + completion broadcast`).
|
||||
- **Where it lives:** `src/lib/services/signing-automation.service.ts` orchestrates the kickoff + cascade. `documents.automation_mode` column tracks `'manual' | 'sequential_auto' | 'concurrent_auto'` (migration `0088_documents_automation_mode.sql`). Webhook handler in `src/app/api/webhooks/documenso/route.ts` reads automation mode on each recipient-signed event and fires the next invite when in `sequential_auto`.
|
||||
- **Files implicated (once built):**
|
||||
- _src/components/documents/active-eoi-card.tsx_ (new "Automate signing" button + state visualisation).
|
||||
- _src/lib/services/documents.service.ts_ (new `automateSigning(documentId, portId)` orchestrator).
|
||||
@@ -541,7 +541,13 @@
|
||||
|
||||
### `/documents/new` CreateDocumentWizard — confusing, redundant pathways
|
||||
|
||||
- **`OPEN`** — _src/components/documents/create-document-wizard.tsx_ + _src/components/documents/new-document-menu.tsx_ + _src/app/(dashboard)/[portSlug]/documents/new/page.tsx_.
|
||||
- **`MOSTLY SHIPPED locally (not yet committed) — remaining: convert page to dialog`**
|
||||
- **What shipped (per commit `2107480` `feat(wizard-refactor): drop inapp pathway + upload branch + per-port template defaults + mark-signed dropdown`):**
|
||||
- Wizard upload branch removed; `source: 'template'` hard-coded.
|
||||
- `pathway: 'documenso-template'` hard-coded; `inapp` removed.
|
||||
- Doc-type-driven template defaults: `/api/v1/documents/template-defaults` returns the per-port `documenso_eoi_template_id` / `documenso_reservation_template_id` / `documenso_contract_template_id`; wizard auto-fills the picker when the rep selects a doc type.
|
||||
- "Mark as signed (offline)" dropdown item exists in NewDocumentMenu (line 113 of new-document-menu.tsx).
|
||||
- **Remaining:** drop the `/documents/new` route in favour of a `<GenerateDocumentDialog>` modal opened from the dropdown — architectural change, deferred until the rest of the launch stabilises.
|
||||
- **React-grab anchor:** `<section class="rounded-md bord..." />` in CreateDocumentWizard in NewDocumentPage.
|
||||
|
||||
**Current state — three flows wired three different ways:**
|
||||
@@ -583,15 +589,14 @@ The catch: most ports will have ~3 templates total (EOI, Reservation Agreement,
|
||||
|
||||
### CreateDocumentWizard — Reminders/Watchers/Signers leak into upload-only flow
|
||||
|
||||
- **`OPEN`** — _src/components/documents/create-document-wizard.tsx_ (the Signers + Reminders + Watchers sections render unconditionally regardless of source).
|
||||
- **React-grab anchor:** `<section class="rounded-md bord..." />` in CreateDocumentWizard.
|
||||
- **Symptom:** the wizard currently shows Signers / Reminders / Watchers sections for every source path. For a flow-3 upload (rep already has the signed PDF, just wants to file it + tag it to an entity), none of those apply — there's no Documenso envelope, no signing event to remind about, no in-flight workflow to watch. The wizard's sections are written for the Documenso-driven flows and bleed into the upload case where they're noise.
|
||||
- **Fix:** hide Signers + Reminders + Watchers when the rep's path is "upload a finished PDF that's already signed offline." Keep them visible for generate-via-template (flow 1) and upload-with-fields-for-signing (flow 2). Today the wizard conflates flow 2 with flow 3 via the misleading "Upload a finished PDF" label — the larger refactor (above) splits them; this fix piggybacks on that split.
|
||||
- **Bundle with:** the wider wizard refactor — same set of edits. Don't ship this in isolation; the section visibility logic depends on the source-path being unambiguous.
|
||||
- **`SUPERSEDED`** — _src/components/documents/create-document-wizard.tsx_ (wizard is generation-only since 2026-05-26 refactor; `source: 'template'` hard-coded, upload branch removed).
|
||||
- **Reason:** the 2026-05-26 wizard refactor cut the upload branch entirely. The wizard is now purely "generate from template → Documenso" so Signers / Reminders / Watchers always apply. Offline-signed upload flows live elsewhere (per-interest external-upload dialogs, generic FileUploadZone). No longer a leak to fix.
|
||||
|
||||
### CreateDocumentWizard subject picker — needs at-a-glance entity scan
|
||||
|
||||
- **`OPEN`** — _src/components/documents/create-document-wizard.tsx:328-370_ (the `grid grid-cols-[max-content_1fr]` subject row).
|
||||
- **`PARTIALLY SHIPPED locally (not yet committed)`** — _src/components/documents/create-document-wizard.tsx_ (subject row).
|
||||
- **Fix applied:** type dropdown → segmented button strip (Interest / Tenancy / Client / Company / Yacht), all 5 types visible at once so the rep clicks once instead of opening a dropdown. Picker below still adapts per-type (existing pickers reused as-is).
|
||||
- **Deferred:** the fully-unified search ("type 'matt'" → mixed-type results) needs a new `<SubjectCombobox>` against `/api/v1/search`. The segmented strip is the high-value 80% fix; the unified search lands when the wider wizard refactor goes through.
|
||||
- **React-grab anchor:** `<div class="grid grid-cols-..." />` in CreateDocumentWizard.
|
||||
- **Symptom:** picking the document subject means choosing a type (Client / Company / Yacht / Interest / Tenancy) THEN searching that one type's picker. Reps don't think in terms of "what type is the recipient" — they think "I need to send this to deal X" or "this is for client Y." The two-step type-then-picker requires the rep to know the answer to the type question before they can search.
|
||||
- **Fix proposal:** replace the type+picker pair with a single unified search field (same idiom as the global Command-search). Typing surfaces matching clients/companies/yachts/interests/tenancies inline, each row carrying its type label as a badge. Recent interactions surface first when the input is empty. The chosen entity sets both `subjectType` and `subjectId` in one click.
|
||||
|
||||
Reference in New Issue
Block a user