Commit Graph

36 Commits

Author SHA1 Message Date
cb8292464c 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>
2026-05-27 22:42:37 +02:00
6caf41651f feat(uat-p5): long-tail polish - tag chips, notes counts, hub context, tenancies toggle
- StageStepper renders now carry tag chips next to the progress bar
  (client interest cards, pipeline summary, preview sheet).
- Notes tab badge on the interest detail aggregates note counts across
  the interest, the linked client, the linked yacht, and any companies
  the client is an active member of - reps see the full surface area
  at a glance.
- Admin Settings: Tenancies Module toggle wired into the Feature Flags
  card. Disabling hides nav/tabs without deleting any rows; re-enabling
  brings them back. Service layer was already complete; this surfaces
  the control on the operations page.
- HubRoot recent-files rows now show folder breadcrumb + entity badge
  (Interest/Client/Yacht/Company) so reps can tell at a glance where a
  file lives. Backed by listFiles enrichment (5 batched lookups per
  page; no per-row queries).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:57:20 +02:00
2592e28578 feat(uat-p4): inheritance polish - yacht dims, occupancy chip, map-flip flag
Phase 4 of the active UAT sweep wraps the inheritance/polish bucket.

- BerthOccupancyChip: new shared component that surfaces the competing
  active interest on a non-available berth as a colour-coded chip with
  a stage badge. Adopted in LinkedBerthRowItem, BerthRecommenderPanel
  recommendation card, and InterestBerthStatusBanner; the banner aligns
  query keys with the chip so React Query dedupes the network call.
- OverviewTab inheritance: getInterestById now ships a yachtDimensions
  block when the interest is linked to a yacht with dimensions. The
  Berth Requirements rows render a "↩ <value> from yacht" pill when
  the desired field is blank; clicking the pill copies the value into
  the interest. After a manual edit, a toast offers to write the new
  value back to the yacht record so the canonical truth stays in sync.
- Map-flip inheritance: ExternalEoiUploadDialog and UploadForSigningDialog
  now expose a single "Mark berth(s) as Under Offer on the public map"
  checkbox that defaults ON when any in-bundle berth already has
  is_specific_interest=true. On submit, PATCHes the in-bundle berths
  that don't already match; sister surface to the EOI generate
  dialog's per-berth picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:48:19 +02:00
8e81670b11 feat(uat-polish): live-UAT round — dialog widths, recommender polish, inline create, tenancy + notes plumbing
Compendium of polish + small-fix work captured during the 2026-05-26
live UAT session. Every change has a corresponding entry in
docs/superpowers/audits/active-uat.md with file:line evidence + root
cause + alternatives considered.

Dialog primitive width
- DialogContent default bumped from sm:max-w-lg (512px) to
  sm:max-w-xl + lg:max-w-3xl so every consumer gets a sane desktop
  default. Confirm dialogs override DOWN, content-heavy dialogs
  override UP.
- FilePreviewDialog full-viewport via w-[min(95vw,1400px)] +
  h-[85vh] so PDFs render at usable width on real desktops.

Recommender card
- Heat badge now a Popover with the score (X/100), the formula in
  plain English, the four component breakdowns (recency / furthest
  stage / interest count / EOI count), and a pointer to the admin
  weight tuning page.
- Area letter span dropped from the card header - mooring number
  already prefixes it.
- BerthRecommenderPanel + the dedicated "Berth Recommendations" tab
  both hidden when interest.desiredLengthFt is null. The empty
  guidance card was reading as noise. interest-tabs.tsx computes
  hasDesiredDims once and gates the inline mount + tab strip
  spread off it.

BerthPicker
- Drop area suffix from row labels. Mooring number already carries
  the area letter prefix; group heading conveys the same context.
  Same fix flows to every BerthPicker consumer (tenancy
  create/renew/transfer, interest form, linked-berths picker).

CreateDocumentWizard
- DOCUMENT_TYPE_LABELS constant added to constants.ts. Wizard reads
  from the map instead of naive replace(/_/g, ' '): "EOI",
  "Contract", "NDA", "Reservation Agreement", "Other".
- "Other" option surfaces a hint pointing the rep at the Title
  field so they describe what the doc actually is.

InterestForm inline client + yacht create
- ClientForm gains an onCreated(clientId) callback. Mutation
  returns { id } in create mode so onSuccess can forward.
- InterestForm renders an "Add new" Button next to the Client label
  (create mode only - hidden on edit), opens ClientForm, auto-
  selects the new client into the draft. Mirrors the existing
  inline yacht-create pattern.
- Reset path includes source: 'manual' alongside the other create-
  mode defaults; the manual flow was dropping back to a blank
  source dropdown on reopen.

Tenancy list
- ClientTenanciesTab activeTenancies query now includes status
  IN ('pending', 'active'). Was filtering to active-only; pending
  rows from manual create + webhook auto-create were invisible on
  the client detail's Tenancies tab.
- TenancyList rows are now keyboard- and click-navigable to the
  tenancy detail page (Enter/Space included). Inner links + buttons
  stop propagation so per-cell navigation works.

NotesList source badge
- Aggregated-mode source badge ("Yacht / Test Yacht") is now a Link
  to the source entity's detail page. New sourceLinkFor helper
  centralises the URL mapping across clients/companies/yachts/
  interests + residential variants.

Yacht transfer audit log
- transferOwnership emits a distinct 'transfer' AuditAction (added
  to AuditAction union in src/lib/audit.ts) with old/new owner
  names resolved at write time. EntityActivityFeed renders
  "Matt transferred owner to Jane Smith" instead of "Matt updated
  this record." formatValueForField unwraps the { name } shape so
  the audit_logs Record<string, unknown> typing stays clean.
- yacht-transfer-dialog copy: dropped "atomic" jargon. Reads "The
  change is logged in the audit history" instead.

Companies autocomplete
- /api/v1/companies/autocomplete now returns the 10 most-recently-
  updated companies when the query string is empty. Was returning
  []. CompanyPicker popover opens with results to scan instead of a
  blank dropdown.

DocumentsHub FlatFolderListing
- Uploaded files (the files table) now merge into the documents
  table view via a parallel /api/v1/files?folderId=X query +
  client-side merge into a unified row list. listFiles service
  honours the folderId filter that was already accepted by the
  validator. New renderFileRow renders file rows with an "Uploaded
  file" type pill + "Stored" status pill, links the filename to
  the download URL. Existing FolderDropZone invalidation covers
  the new query, so drag-drop and New-document-menu uploads
  refresh the list without a page reload.
- FlatFolderListing wrapped in a vertically-spaced container so
  subfolders / search row / list have consistent gap.
- Per-row chevron only renders when totalSigners > 0; empty
  placeholder column kept so grid alignment doesn't jump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:07:45 +02:00
e9509dc45c chore(audit-drain): rip out next-intl, RTL lint, sweeps, polish
Drain the long-tail audit queue captured in alpha-uat-master.md.

- next-intl ripped out (zero useTranslations callers ever existed):
  package.json, next.config.ts plugin wrap, src/i18n/, messages/, and
  the layout NextIntlClientProvider all gone; <html lang="en"> hardcoded.
- RTL lint nudge added: warn-only no-restricted-syntax on physical
  Tailwind utilities (ml-/mr-/pl-/pr-/text-left/text-right/border-l/
  border-r/rounded-l-/rounded-r-) inside JSX className literals.
  Existing ~1,000 sites grandfathered; new code trends toward logical.
- Icon-only button accessibility lint: jsx-a11y/control-has-associated-
  label enabled at warn; 4 empty <th>/<td> action placeholders gain
  sr-only labels.
- Currency: SUPPORTED_CURRENCIES drops the hardcoded English labels;
  new currencyLabel(code, locale?) helper resolves via Intl.DisplayNames.
  CurrencySelect + settings-manager migrated.
- Date locale sweep: 7 surfaces flip from toLocaleString('en-GB'|'en-US')
  to toLocaleString(undefined, ...) so dates honour runtime locale.
- Dialog/Sheet width: 10 document/EOI/entity-form dialogs gain a
  lg:max-w-4xl or lg:max-w-5xl step so wide desktops get breathing room.
- PaymentsSection collapsed-bar: slim one-line bar showing
  "Payments - Not received yet" or "Payments - \$X received - N payments
  - Expand"; per-interest collapse state persists in localStorage; the
  RecordPayment flow auto-expands.
- muted-foreground opacity sweep: 10 text-bearing
  text-muted-foreground/{60,70,80} hits dropped to plain
  text-muted-foreground for AA contrast on muted bg. Icon-only
  (aria-hidden) opacity hits left as-is.
- Micro-type bump: text-[10px] and text-[11px] -> text-xs (12px)
  across 87 files in src/components + src/app. Pure mechanical sweep.
- Audit-doc cleanup: alpha-uat-master.md stale 2026-05-25 summary
  rewritten with cumulative state through today. Items genuinely still
  open are now a short long-tail list.
- New docs/marketing-site-followups.md: Umami Phase 4a/3/5, email
  pixel E2E verification, and website-cutover work parked here so
  they don't get lost in the CRM audit doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:48:46 +02:00
14ae41d0fa feat(uat-b1): ship Wave A-E of Bucket 1 audit findings
Wave A (Interest+EOI form quick wins):
- Auto-select yacht after inline-create from interest form
- EOI generate dialog: "View EOI" action toast
- Interest form berth picker: formatBerthRange compact label
- Remove "Generate EOI" button from Documents tab (clean removal)
- Interest auto-assign: only sales_agent/sales_manager auto-claim
  ownership on create (explicit role check via user_port_roles join)
- LinkedBerthRowItem dims: drop "D" suffix + "L × W" format
- ExternalEoiUploadDialog: prefillSignatories prop threaded from
  active EOI signers
- EOI signature progress on Overview milestone card footer

Wave B (a11y + i18n sweeps):
- aria-live on supplemental-info error state
- text-[10px] -> text-xs in client-pipeline-summary
- Currency formatter: locale default removed (Intl uses runtime)
- en-US/en-GB hardcoded toLocaleString swept across 13 components

Wave C (Primary berth always in EOI bundle):
- Service guard strengthened on update path
- Migration 0083 backfills historical primary rows

Wave D (Onboarding super_admin discoverability):
- /api/v1/admin/onboarding/status endpoint + shared service
- Topbar OnboardingBanner (super_admin, session-dismissible)
- OnboardingTile dashboard widget (rail group, self-hides at 100%)
- Celebration toast + invalidate of shared status on last tick

Wave E (Branded post-completion email idempotency):
- Verified handleDocumentCompleted already owns the email fan-out
- Added regression test for the polling path + idempotency

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 03:40:37 +02:00
221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00
91be0f9136 feat(uat-batch): M43 — form-template bindings + inline field history
Closes plan item 43 (Form-template fields bind to Interest/Client data —
autofill, override-preservation history, dual-surface audit trail).

Phase 1 — Editor:
- New bindable-fields catalog (src/lib/templates/bindable-fields.ts):
  client/yacht/interest paths, each tagged with the entity, column, and
  default input type. Source of truth for what can bind + what
  interest_field_history.field_path strings the writers should use.
- formFieldSchema gains optional bindTo, validated against the catalog
  as an allow-list (no arbitrary paths sneak through).
- form-template-form admin sheet: per-field "Bind to" dropdown grouped
  by entity, auto-derives label/key/type when a binding is picked,
  shows "Autofills from + writes back to {label} . {path}" badge.

Phase 2 — Runtime + history writes:
- supplemental-forms.service.applySubmission already wrote
  interest_field_history rows for client name/email/address from the
  earlier 0081 migration session. Extended to also capture phone +
  yacht (name, length, width, draft) diffs that were silently going
  to the entity without an audit row, and to push insert-path
  overrides for the no-existing-address case.
- Field paths aligned with the bindable-fields catalog so detail-page
  lookups work via exact-match WHERE field_path = ?.

Phase 3 — Inline history surface:
- New /api/v1/clients/[id]/field-history (mirror of the existing
  interests endpoint).
- shared/field-history: FieldHistoryProvider wraps a detail tab and
  fires a single keyed GET; FieldHistoryIcon consumes the context and
  renders a small clock affordance only when at least one override
  exists, opening a popover with the reverse-chrono diff list.
- Client + Interest detail Overview tabs wrapped in the provider;
  EditableRow gains an optional historyPath prop; ContactsEditor
  renders the icon next to the canonical primary email/phone.

1454/1454 vitest, tsc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:51:39 +02:00
94c24a123a feat(uat-batch): Groups F + G + H — DocsHub/signing + admin consolidation + email
F27–F29, G30, G31, H32, H33 from the 2026-05-21 plan.

Shipped now:
  F28  Past-milestones expandable history. The Past strip on the
       Interest overview becomes an <Accordion> — each row collapses
       to the same one-line summary as before, expands to render the
       full <MilestoneSection> (steps list, sub-status, inline doc
       actions). Reuses the existing MilestoneSection so no new
       per-milestone rendering needs to be maintained.
  F29  Watchers configurable at document creation time. The unified
       create-document wizard gets a Watchers section with a
       multi-select checkbox list backed by /api/v1/admin/users/picker.
       Selected user ids are sent in the `watchers` array on the POST
       (replacing the prior hardcoded `[]`). UI matches the
       post-creation WatchersCard so reps see the same identity rows
       regardless of entry point.
  G30  /admin/invitations merged into /admin/users. The Users page
       now wraps the existing UserList + InvitationsManager in a
       Tabs control (Active users / Invitations). The standalone
       /admin/invitations route returns a redirect to the merged page
       for bookmark back-compat. Removed nav catalog entry +
       admin-sections-browser tile; extended the Users catalog
       keywords with "invitations / pending invites / onboarding"
       so command-K search still lands on the right surface.
  G31  /admin/ai picks up the berth-PDF-parser section + a "planned
       AI surfaces" placeholder. Berth PDF parser remains
       env-configured today; the page now documents it so admins
       don't hunt for the controls. Closes the "where do I configure
       AI?" loop.
  H32  Email settings explainer panel above the SMTP cards. Spells
       out why noreply + sales have separate credentials and which
       workflows ship from each mailbox. Existing field titles
       gained the "(noreply)" suffix so the model maps cleanly.
  H33  Supplemental-info-request email rebuilt to use the shared
       branded shell (logo + blurred overhead background + max-
       width 600 table layout) instead of the prior plain-HTML
       page. Per-port branding (logo / primary color / background /
       header / footer) flows from getPortBrandingConfig. CTA
       button picks up the port's primary color.

Already shipped (verified pre-shipped):
  F27  DocumentsHub root view already hides the breadcrumb via
       `selectedFolderId !== undefined` conditional.

Verified: tsc clean, vitest 1454/1454.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:40:48 +02:00
991e2223c7 feat(uat-batch): Group C Berth list features (3 new ships + 1 verified)
C20–C23 from the 2026-05-21 plan.

Shipped now:
  C21  Dimensions ft/m column toggle persisted to user prefs.
       `TablePreferences.dimensionUnit` ('ft' | 'm') added to the user-
       profiles JSONB. `useTablePreferences` returns `dimensionUnit` +
       `setDimensionUnit` alongside hidden/density. New
       `getBerthColumns(unit)` factory rewrites the dimensions /
       nominalBoatSize / waterDepth cells when ft is requested
       (waterDepth converts on-the-fly from the canonical meters
       column at 3.2808 ft/m). Berth-list toolbar gains a small
       ft/m toggle button next to the density toggle.
  C22  ft/m switching on Berth Requirements rows.
       `interest-tabs.tsx` Berth-requirements section now honours
       `interest.desiredLengthUnit`. Labels flip to "(m)" when set;
       value reads from `desired*M` columns; on save, both the chosen-
       unit and the canonical counterpart columns are PATCHed (3.28084
       ratio) so downstream surfaces (recommender, EOI merge fields)
       stay in lockstep. `InterestPatchField` widened with `desired*M`
       variants.
  C23  Berth list bulk-edit affordance.
       New `POST /api/v1/berths/bulk` (mirror of /interests/bulk):
       discriminated union of `change_status` / `change_tenure_type` /
       `add_tag` / `remove_tag` / `archive`, 500-id cap, per-row
       failure reporting, single `berths.edit` permission gate
       (no separate `archive` perm exists on berths today). Status
       mutations route through `updateBerthStatus` so under-offer /
       sold transitions still trigger the primary interest_berths
       auto-link + the rules-engine evaluation.
       BerthList toolbar wires `bulkActions` on the DataTable —
       Change status (Select dialog), Change tenure (permanent /
       fixed-term), Add tag, Remove tag, Archive (destructive +
       confirmation). Each dialog uses the same `bulkMutation` so
       toast + cache-invalidation behaviour is consistent across
       actions.

Already shipped (verified):
  C20  Berth list rates / pricing valid columns hidden by default —
       already in `BERTH_DEFAULT_HIDDEN`.

Verified: tsc clean, vitest 1454/1454.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:22:30 +02:00
7ecf4ee813 feat(uat-batch): Group B Interest detail polish (5 new ships + 2 verified)
B13–B19 from the 2026-05-21 plan. Five new ships; two items already in
place from earlier work but flagged for verification.

Shipped now:
  B14  Interest Overview Email + Phone rows: new <ClientChannelEditor>
       combobox. Primary value renders inline (free-text for email,
       <InlinePhoneField> for phone with country picker). Chevron opens
       a popover listing every contact in the channel — promote to
       primary, delete non-primaries, or inline-add a new contact.
       Backed by the existing /clients/[id]/contacts CRUD + promote-
       to-primary endpoints. Wired into the Email + Phone rows on
       interest-tabs.tsx Overview.
  B15  Inline phone editor: the phone branch of <ClientChannelEditor>
       uses <InlinePhoneField> (country code + national-format split).
       interests.service.ts now returns `clientPrimaryPhoneCountry` so
       the editor can preserve the ISO-3166-1 alpha-2 round-trip.
  B16  Client Overview interest summary: PanelVariant of
       <ClientPipelineSummary> renders a one-line "Wants L × W × D ·
       Source" under each interest's header when constraints / source
       are captured. Hidden when both are empty.
       <ClientInterestRow> type extended with the new fields; the
       /api/v1/interests query already returns them.
  B17  Notes Latest-note teaser stage pill: stage-badge chip next to
       the "5 minutes ago · Matt" line. Shows the deal's CURRENT
       pipelineStage — a stage-at-note-time lookup would require a
       per-render audit_logs read, over-engineered for a context hint.
  B18  InterestBerthStatusBanner names + links the competing deal:
       reuses /berths/[id]/active-interests endpoint shipped in 292a8b5;
       one query per conflicting berth via useQueries. Picks the
       isPrimary competing interest (falls back to first non-self
       row); renders an inline <Link> to the competing detail page.

Already shipped (verified pre-shipped):
  B13  Inbox Reminders embedded filter row — `embedded` prop already
       wired in reminder-list.tsx.
  B19  Qualification auto-confirm intent at stage ≥ EOI — already
       handled by computeAutoSatisfied's `stageIdx > qualifiedIdx`
       gate (covers eoi / reservation / deposit_paid / contract).

Verified: tsc clean, vitest 1454/1454.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:08:41 +02:00
f0dbefcac2 chore(copy): em-dash sweep across user-facing JSX text + bump lint to error
Replaced 174 em-dashes (—) with " - " (space-hyphen-space) across 49
files in src/components + src/app. The em-dash reads as a tell-tale
"AI-generated" marker per the user's design feedback; hyphens with
spaces preserve the connector semantics without the AI tint.

Touched only lines outside pure-comment context (// /* * */). Code
comments, JSDoc, audit-log strings, structured logging strings, and
templates outside the lint scope retain their em-dashes for now —
they're not user-visible.

Also captured two remaining cases that used the `&mdash;` HTML entity
instead of the literal character (system-monitoring-dashboard,
interest-stage-picker) — replaced with a plain hyphen.

Bumped the existing `no-restricted-syntax` rule from `warn` → `error`
in eslint.config.mjs scoped to src/components/**/*.tsx +
src/app/**/*.tsx. New code reintroducing em-dashes in JSX text now
fails the lint gate.

Verified: tsc clean, vitest 1448/1448, eslint 0 em-dash warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 20:02:58 +02:00
f39f0aa7bc feat(uat-batch-16): Interest Overview cleanup — hide legacy reminder panel, deprioritize PaymentsSection
Two coordinated layout changes on the interest Overview tab so the
active milestone gets visual priority.

- Legacy `interest.reminderEnabled` panel removed from Overview. The
  field still drives the auto-follow-up worker
  (`processFollowUpReminders`) and the REMINDERS section + bell-in-
  header surface active reminders, so the read-only duplicate panel
  was pure noise. Backend behaviour unchanged; no schema impact.
- PaymentsSection mount relocated from above the milestone strip to
  below it. The active milestone above carries the rep's day-to-day
  attention; deposits-tracking is reference / history once expected.
  Render order: past strip → current milestone(s) → future
  (collapsed) → PaymentsSection → Lead/Source grid. Pre-Reservation
  the section still doesn't render at all (unchanged). Collapsed-bar
  + summary-chip refinement parked.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:27:17 +02:00
db511063df feat(uat-batch-10): copy polish, TTL trim, and a11y discrete fixes
- Supplemental-info link TTL trimmed from 30 → 14 days (single
  constant in supplemental-forms.service).
- LinkedBerthsList toggle renamed "Mark in EOI bundle" →
  "Include in EOI"; tooltip aria-label updated to match.
- Icon-only row-action triggers on the interest / client / berth list
  tables gain aria-label (Row actions for <name>) so SR users hear
  the row context.
- Table / Board view toggle on interest list gains aria-label +
  aria-pressed on each variant; wrapper gets role="group".
- Upcoming-milestones disclosure on interest-tabs gains
  aria-expanded + aria-controls; recommender Hide/Add filters
  button matches.
- BrandedAuthShell logo alt no longer defaults to "Sign in" — uses
  the configured `appName` when known, empty string otherwise so
  screen readers don't announce "Sign in" on password-reset /
  set-password pages.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:01:17 +02:00
d8da1f634d feat(uat-batch-9): milestone classifier + skip-ahead backfill controls
Two coordinated UX changes that finally make the rep's manual-stage-
jump workflow legible:

- Milestone phase classifier introduces a "stage-owning milestone"
  rule. When the rep manually advances the deal to Reservation+ but
  earlier sub-statuses are still un-signed, the current-stage
  milestone now stays marked `'current'` (no longer collapses into
  the past-strip / upcoming-accordion based on completion alone).
  Earlier-than-stage milestones bucket to `'past'` so the rep can
  backfill them; later slots stay `'future'`. The previous
  firstIncompleteKey-driven rule still applies in stages without an
  owning milestone (enquiry / qualified / nurturing).
- Skip-ahead backfill control `<MilestoneBackfillButton>` lands in
  the past-milestones strip whenever a milestone's date column is
  null. Opens a DatePicker popover (today default, accepts any past
  date) and PATCHes the relevant date_* column directly via
  useInterestPatch — no stage transition fires.
- `InterestPatchField` extended with the five milestone date keys;
  validator gains `dateDepositReceived` (was the only missing one).

Together this means: a deal manually-advanced from EOI Sent → Deposit
no longer hides Reservation under upcoming-milestones AND the rep can
record the EOI/reservation signing dates without re-triggering the
stage transition.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:54:33 +02:00
8f42940c52 feat(uat-batch-3): wave-1 primitives — DatePicker, DateTimePicker, FileInputButton, ColumnPicker hideAll
Builds the foundational primitives that subsequent waves depend on.
None of these introduce new deps — date-fns, react-day-picker, and
shadcn Calendar were already in the tree.

- `<DatePicker>` and `<DateTimePicker>` in src/components/ui — desktop
  popover wrapping the existing shadcn Calendar (caption-dropdown nav
  so reps can jump months/years for the SkipAheadBanner backfill UX),
  mobile native input via useIsMobile. Drop-in for `<Input type=date>`
  / `<Input type=datetime-local>`.
- `<FileInputButton>` in src/components/ui — styled Button + hidden
  input, replaces browser-default file picker UI. Most queued sweep
  sites already used the hidden-input + Button-trigger pattern; the
  primitive lands for any new caller plus consistent filename display
  + clear button.
- ColumnPicker `hideAll()` footer item — symmetric to existing
  `showAll()`, with the same visibility gate. Lands platform-wide via
  the shared component.
- Migrated highest-leverage call sites to the new primitives:
  * MilestoneAdvanceButton (backfill UX)
  * Reminder form (datetime-local → DateTimePicker)
  * Snooze dialog (datetime-local → DateTimePicker)
  * External-EOI upload dialog (date + file picker)
  * Payments section (received-on date)
- Remaining 15+ date-input call sites parked for a follow-up sweep —
  several use react-hook-form `register` patterns that need careful
  migration to the new controlled-value contract.

tsc clean. 1419/1419 vitest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:10:02 +02:00
2d574172ec fix(uat-batch-1): wave-1 blocker bugs — supplemental gate, file FK, downloads, search dedup, notes stale, expense form, vocab
Surgical fixes for the 7 UAT blockers that prevent productive forward
testing. Each item has a corresponding entry in alpha-uat-master.md.

- supplemental-info route relocated out of (portal) so it bypasses the
  isPortalDisabledGlobally() kill-switch. URL unchanged.
- file upload service derives client_id/company_id/yacht_id from
  (entityType, entityId) when not explicitly passed, so interest-tab
  uploads no longer land with client_id=NULL and stay visible in the
  Attachments list.
- triggerBlobDownload / triggerUrlDownload helpers in src/lib/utils
  attach the anchor to the DOM before click so Chromium honours the
  download attribute; 7 sites refactored, file-named downloads stop
  arriving as bare UUIDs.
- search-nav-catalog dedupes by href at the result-collection layer so
  the same href can no longer surface twice in the command-K dropdown
  (kills the React duplicate-key warning); /admin/templates entries
  merged into a single richer-keyword variant.
- NotesList gains a parentInvalidateKey prop, wired through all five
  callers (interest, client, yacht, company, residential client/
  interest) so the Overview "Latest note" teaser refreshes when a note
  is added in the Notes tab.
- expense-form-dialog: setValue('receiptFileIds') / setValue(
  'noReceiptAcknowledged') on upload/clear/checkbox so the schema-level
  refine sees the field and Create stops silently no-op'ing on submit.
- bulk-add-berths-wizard: side-pontoon dropdown now reads through
  useVocabulary('berth_side_pontoon_options') instead of a wrong local
  enum ('Port', 'Starboard', 'Bow', 'Stern') — wizard data now matches
  the rest of the platform + honours admin-editable per-port overrides.

tsc clean. 1419/1419 vitest. lint clean on touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 16:50:58 +02:00
449b9497ab fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes;
single grouped commit so the index doesn't fragment into 30 one-liners.

User & auth:
- `user-settings`: name now updates the avatar + topbar menu after save
  (was reading stale session).
- `me/password-reset`: 3 bugs (token validation, error response shape,
  redirect chain).
- Admin user permission-overrides route honours the same envelope as
  the rest of the admin surface.

Dashboard:
- Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card`
  (replaced by the customisable widget grid).
- Strip `revenue_breakdown` from analytics route + use-analytics +
  service + integration test so nothing renders an empty card.
- Activity log timeline overshoot fix (`interest-timeline` +
  `entity-activity-feed`).
- Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile.
- `dev-mode-banner`: derive dismissed state synchronously instead of
  via an effect (set-state-in-effect lint rule).

Forms & lists (assorted polish):
- client / company / yacht / interest / reminder forms — validation +
  empty-state copy + tab transitions.
- companies/yachts list tweaks; berth recommender panel; qualification
  checklist; supplemental info request button.

Infra & misc:
- Queue workers (ai / email / notifications) — log shape +
  per-job timeout consistency.
- Auth / brochures / users schema small adjustments; seeds reflect
  permissions matrix changes.
- Scan shell + scanner manifest + AI admin page small fixes.
- `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react`
  (recommended config from echarts-for-react inside Next).

Docs:
- `docs/superpowers/audits/alpha-uat-master.md` — single rolling
  cross-cutting UAT findings doc (per CLAUDE.md convention).
- `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log
  normalization (§J).
- 2026-05-18 audit log updated with this batch.
- `CLAUDE.md` — small manual UAT scaffold notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:56:11 +02:00
ef0dc5abc4 feat(post-audit): finish Phase 3 / 4 / 5 / 7 — remaining work
Phase 3 — EOI overrides (now ☑):
- Address override field with the same per-component input UX as the
  canonical address form (line1/line2/city/state/postal + ISO
  subdivision + CountryCombobox). Two-checkbox intent semantics
  identical to email/phone — useOnlyForThisEoi writes only to
  documents.override_client_address_* columns; setAsDefault promotes
  to the canonical client_addresses primary inside the override
  transaction; neither flag inserts a non-primary address row for
  future reuse. eoi-context route now returns available.addresses so
  the dialog can render the picker over existing rows.
- yachts.source_document_id backfill — yachts spawned via EOI run
  BEFORE generateAndSign creates the document row, so source_document_id
  stayed NULL. Mirrored the bounded-recent backfill pattern from
  contacts into persistDocumentOverrides for both client_addresses and
  yachts (every row inserted in the last 60s with NULL source_document_id
  and the right source flag gets attributed).
- Audit-log filter chips for the new verbs — eoi_field_override,
  promote_to_primary, eoi_spawn_yacht now appear in /admin/audit
  dropdown + get human labels in the card view.

Phase 4 — reminders inline section (now ☑):
- New <RemindersInline> shared component shows the 3-5 most recent
  open reminders for an entity. Mounted on Overview tab of yacht /
  client / interest detail. Empty state hints at the header button
  rather than duplicating it.

Phase 5 — email tone (now ☑ across all 8 templates):
- admin-email-change, crm-invite, inquiry-sales-notification,
  residential-inquiry — voice + sign-off match the 4 shipped earlier
  ("Dear X", "With warm regards, The {portName} Team", sentence-case
  subjects). Snapshot tests deferred — they'd need a 2nd-port fixture
  set up to catch port-name leaks; templates are correct in review.

Phase 7 — PDF editor (now ☑):
- 7.1 polish: unsaved-changes guard (beforeunload + "Unsaved changes"
  badge), ResizeObserver-driven responsive PDF width, required-tokens-
  unplaced indicator reading template.mergeFields.
- 7.2 drag-to-move with on-page clamping.
- 7.2 four-corner resize handles with min-size enforcement.
- 7.2 right-click context delete via onContextMenu.
- 7.2 multi-page navigation + per-page marker filter.
- 7.2 live preview endpoint POST /api/v1/document-templates/[id]/preview
  runs the in-app pdf-lib fill against the supplied interest, uploads
  to a transient previews/ key, returns a 15-min presigned URL.
- 7.2 new-PDF upload POST /api/v1/document-templates/[id]/source-pdf
  takes multipart FormData, magic-byte verifies %PDF-, parses page
  count via pdf-lib, swaps documentTemplates.sourceFileId. Editor
  warns when the new page count truncates the prior set.

Quality gates: 1374/1374 vitest, tsc clean, lint 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:09:19 +02:00
0f99f054b3 feat(post-audit): batch A+B quick-wins + audit-side residuals
Bundles the user-prioritised follow-ups from the post-audit punch-list.

Batch A — pipeline + EOI safety:
 - §1.1 timeline buildAuditDescription renders diff fields ("leadCategory → hot_lead").
 - §4.13 EOI rejection cascade: notification to assigned rep + audit row + rose banner.
 - §4.10b finish doc-detail: SigningProgress reuse, linked-entity names (server-resolved),
   per-event icons + tooltips + show-more in activity panel.
 - §7.2 stage guidance card replaces empty Payments slot pre-reservation.
 - §4.15 deal-pulse trigger audit (docs/deal-pulse-trigger-audit.md).

Batch B — UX consistency + docs:
 - §1.4 quick log-contact button on interest header.
 - §2.1 contact-log compose: Dialog → Sheet.
 - §7.1 docs/deal-pulse explainer page; /docs/ in PUBLIC_PATHS.
 - DocumentStatus now includes 'rejected' + 'declined' across constants, labels, tone maps.

Audit-side residuals:
 - M-NEW-1 /me/ports skips port-context requirement.
 - M-AU03 audit log CSV export endpoint + UI button.
 - M-IN03 dead receipt-scanner.ts deleted; live path already per-port.
 - M-P01 pg_trgm GIN indexes (migration 0071).
 - §10.1 webhook tests verified passing (was stale).

Deferred per user direction:
 - §11.3 email copy refactor (needs old-CRM reference).
 - M-EM03 IMAP bounce-to-interest linking.

Tests: 1374/1374. tsc + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:22:11 +02:00
4b5f85cb7d fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).

CRITICAL (3):
 - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
   no longer silently drop interest links
 - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
 - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
   callers must go through /stage with the override-guard chain

HIGH (14/15):
 - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
   interests/documents/reservations/reminders/invoices (migration 0070)
 - H-02 login page reads ?redirect= param with same-origin guard
 - H-03 CRM invite token moves to URL fragment so it never lands in
   nginx access logs / Referer headers
 - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
 - H-05 toggleAccount writes an audit row
 - H-06 upsertSetting masks any value whose key ends with _encrypted
 - H-07 archiveClient cascade fires per-interest audit rows
 - H-08 createSalesTransporter applies SMTP_TIMEOUTS
 - H-09 AppShell stable children — viewport flip across breakpoint no
   longer destroys in-progress form drafts
 - H-10 portal documents page swaps Unicode glyph status icons for
   Lucide CheckCircle2/XCircle/Circle + aria-labels
 - H-12 list components swap alert(...) for toast.warning(...)
 - H-13 5 icon-only buttons gain aria-label
 - H-14 parseBody treats empty bodies as {}
 - H-15 admin layout renders a 403 panel instead of silent bounce
 - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet

MEDIUM (28+):
 - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
   WHEREs across custom-fields, notes (all 6 entity types x update +
   delete), client-contacts, yacht ownerClient lookup, webhook reads
 - M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
 - M-EM01 portal-auth emails thread through portId
 - M-EM02 sendEmail accepts cc/bcc params
 - M-EM04 notification_digest catalog key
 - M-IN01 portal presigned download URLs use 4h TTL
 - M-IN02 OpenAI client lazy-instantiated
 - M-IN04 stale pdfme refs updated to pdf-lib AcroForm
 - M-IN05 umami.testConnection returns tagged union
 - M-L01 reservations tenure_type unified with berths
 - M-L02 report-generators canonicalize stage values
 - M-AU01 audit log placeholder copy fixed
 - M-AU04 outcome_set / outcome_cleared distinct audit verbs
 - M-NEW-2 activity feed entity name+type separator
 - M-R01 portal allowlist narrowed + portal_session backstop in proxy
 - M-SC02 companies archived partial index
 - M-SC04 audit_logs.searchText documented as DB-managed
 - M-S01 storage_s3_access_key_encrypted admin field
 - M-U01 audit log empty state uses <EmptyState>
 - M-U09 invoice delete dialog -> <AlertDialog>
 - M-U10 toast.success on ClientForm + InterestForm create/edit
 - M-U11 settings-form-card logo preview alt text
 - M-U14 mobile topbar title on clients/yachts/interests/berths
 - M-U15 Invoices in mobile More-sheet

LOW (6/8):
 - L-AU01 severity defaults for security-relevant verbs
 - L-AU02 +13 missing actions in admin audit filter
 - L-AU03 +7 missing entity types in admin audit filter
 - L-AU04 dead listAuditLogs stubbed
 - L-D02 CLAUDE.md Owner-wins chain tightened

Bonus — Document detail polish (#67 partial, 3/6 deliverables):
 - state-aware action button per signer
 - watcher Add UI with display-name resolution
 - cleanSignerName cleanup

Prior session work bundled in:
 - Documenso v2 webhook + envelope-ID normalization + sequential signing
 - SigningProgress UI redesign (avatars, per-signer state, timestamps)
 - env->admin settings registry + RegistryDrivenForm + encrypted creds
 - Embedded-signing card + Test connection + setup help
 - Dev-mode EMAIL_REDIRECT_TO banner
 - Pipeline rules admin page
 - Sales email config card
 - Audit log details Sheet
 - EOI tab: Finalising badge, absolute timestamps, sequential indicator
 - Notes pipeline_stage_at_creation (migration 0069)
 - Documenso numeric ID dual-key webhook (migration 0068)
 - Dimensions criterion copy (migration 0067)

Tests: 1374/1374 vitest pass. tsc clean. lint clean.

See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
6b28459c45 feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.

Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
  three doc-status columns, two documenso-id columns, and
  date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
  interest_qualifications (per-interest state), payments (deposit /
  balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
  the new stage + doc-status + outcome shape.

Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).

v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
       the contact-log compose dialog (useVoiceTranscription hook).
- C:   berth-rules-engine wraps state writes in pg_advisory_xact_lock
       with an idempotent re-read; emits rule_evaluated audit traces.
- D:   Documenso webhook: reservation/contract sub-status stamping
       moved out of the PDF-download try-block so a download failure
       no longer swallows the stamp. New integration test coverage.
- E:   /admin/qualification-criteria CRUD page + admin component.
- F:   default_new_interest_owner exposed in System Settings.
- G:   recentActivityCount + active_engagement deal-pulse signal
       surfaced as a chip on interests + hot-deals card.
- H:   interest_assigned notification on assignedTo change (skips
       self-assign, uses a dedupe key).

Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.

Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
c8ea9ec0a0 fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:

- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/

The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.

Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.

Test suite stays at 1315/1315 vitest. typescript clean.

Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
4233aa3ac3 fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to
Sheet side=right so every detail-preview surface uses the same
primitive. Document the doctrine: Sheet for side panels on both desktop
and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX
(currently just MoreSheet).

Closes ui/ux M11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 11:50:07 +02:00
3ffee79f3f feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones
Mobile + responsive
- berth-form full-width on phones (was 480px fixed → overflowed iPhone)
- currency-input switched to inputMode=decimal with live thousands separator
- client-form Country/Timezone/Source/Preferred-Contact full-width <sm
- contacts row restructured so Primary toggle + Remove get their own strip
- customize-dashboard footer stacks vertically on mobile; Done full-width
- interest-form client/berth pickers no longer cmdk-filter on UUID (typing
  "Carlos" now returns Carlos Vega instead of "No clients found")

Data + consistency
- SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces
  now resolve interest/client source from one place
- INTEREST_OUTCOMES adds lost_other (picker, badge, timeline)
- Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort
- archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles
- TableBody last-row uses border-b-0 (not border-0); colored left-accent
  on the bottom berth row now renders
- Hide Invite-to-Portal until port setting === true (was !== false default-show)
- OwnerPicker primer query resolves entity name on first paint (no more
  UUID flash before the popover opens)

Terminology
- Replaced user-facing "Documenso" with "signing service" / "Generated EOI" /
  "Manual EOI" in 8 components (admin/internal references kept)
- Plainer status-change copy on berth-detail-header

Forms + editing
- InlineEditableField gained a `date` variant (native picker); applied to
  company incorporation date and ready for other YYYY-MM-DD plaintext fields
- Inline source picker on interest-tabs detail (was free text)
- TagPicker self-hides when port has no tags AND nothing is selected
- New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom)
- Compose dialog follow-up is now a toggle that reveals datetime picker

Pipeline milestones
- changeStageSchema accepts optional milestoneDate; service stamps it on the
  matching date column instead of always using now
- MilestoneAdvanceButton popover collects a back-date before stage advance
- Applied to every "Mark X manually" surface on the interest overview

EOI / linked-berths polish
- Add-bypass row aligned inline with toggle descriptions
- Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their
  legal vs. public-map consequences

Surfaces
- Companies list now has the column picker + persisted hidden-column prefs
- NotesList aggregate flag enabled on clients, companies, residential_clients
  (yachts already aggregated)

ft/m unit toggle (interim, before drift fix)
- "Berth size desired" gets a section-level ft/m toggle; per-field hint shows
  the converted value. Storage stays canonical-ft for now; the drift-safe
  persistence migration is the next step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:50:58 +02:00
3e4d9d6310 feat(interests): EOI/contract/reservation tabs + contact log + berth interest milestone + interest list overhaul
Major interest workflow expansion driven by the rapid-fire UX session.

EOI / Contract / Reservation tabs replace the generic Documents tab when
the deal is at the relevant stage — workspace pattern with active-doc
hero, signing progress, paper-signed upload, and history strip. Stage-
conditional visibility wired through interest-tabs.tsx so the tab set
shrinks/expands as the deal moves through the pipeline.

Contact log: per-interaction structured log (channel/direction/summary/
optional follow-up reminder). New `interest_contact_log` table + service
+ tab UI (timeline with channel-coded icons + compose dialog).
auto-creates a reminder when followUpAt is set.

Berth Interest milestone: first milestone in the OverviewTab's pipeline
strip, completes the moment any berth is linked via the junction. Drives
the "have we captured what they want?" sanity check for general_interest
leads before they move to EOI.

Stage-conditional milestones: past phases collapse into a one-liner
strip, current phase expands, future phases hide behind a "Show
upcoming" toggle. Inline stage picker now defers reason capture to an
override-confirm view (only required for illegal transitions, not the
default flow).

Notes blob → threaded: dropped `interests.notes` column entirely; the
threaded `interest_notes` table is the single source of truth. Latest-
note teaser on Overview links into the dedicated Notes tab. Polymorphic
notes service gains aggregated client view (unions client + interest +
yacht notes with source chips and group-by-source toggle).

Berth interest list overhaul:
  - Configurable columns via ColumnPicker (18 toggleable, 5 default-on)
  - Natural-sort SQL ORDER BY on mooring number (A1, A2, A10 not A10, A2)
  - Per-letter row tinting via colored left-border accent + dot in cell
  - Documents tab merged Files (single attachments section)

Topbar improvements:
  - Always-visible back arrow on detail pages (path depth > 2)
  - Breadcrumb-hint store + useBreadcrumbHint hook so detail pages can
    push their entity hierarchy (Clients › Mary Smith › Interest › B17)
  - Tighter spacing, softer separators, 160px crumb truncation

DataTable upgrades:
  - Page-size selector with All option (validator cap raised to 1000)
  - getRowClassName slot for per-row styling (used by berth tinting)
  - Fixed Radix SelectItem crash on empty-string values via __any__
    sentinel (was crashing every list page that opened a select filter)

Interest list:
  - Configurable columns picker
  - Stage cell clickable into detail
  - TagPicker + SavedViewsDropdown sized h-8 to match adjacent buttons
  - Save view moved into ColumnPicker menu; Views button hidden when
    no views are saved
  - Pipeline kanban board endpoint at /api/v1/interests/board with
    minimal projection, 5000-row cap + truncated banner, filter
    pass-through

Mobile chrome + sidebar collapse removed (always-expanded design choice).

User management lists super-admins (was inner-joined on user_port_roles
which excluded global super-admins).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:59:28 +02:00
Matt Ciaccio
b4776b4c3c feat(interests): linked berths list with role-flag toggles + EOI bypass
Implements plan §5.5: a per-interest "Linked berths" panel mounted above the
recommender on the interest detail Overview tab. Each junction row exposes
the role-flag controls reps need to manage the M:M `interest_berths` link
without the legacy single-berth flow.

UI (`src/components/interests/linked-berths-list.tsx`)

* Rows ordered with primary first; mooring number links to /berths/[id], with
  area + a status pill (available/under_offer/sold) and a "Primary" chip.
* "Specifically pitching" Switch (writes `is_specific_interest`) with the
  consequence text from §1: "This berth will appear as under interest on the
  public map" / "This berth is hidden from the public map".
* "Mark in EOI bundle" Switch (writes `is_in_eoi_bundle`).
* "Set as primary" button when the row isn't primary - the existing
  `upsertInterestBerth` helper demotes the prior primary in the same tx.
* "Bypass EOI for this berth" with reason textarea, ONLY rendered when the
  parent interest's `eoiStatus === 'signed'`. Writes the bypass triple
  (`eoi_bypass_reason`, `eoi_bypassed_by` = caller, `eoi_bypassed_at` = now);
  also supports clearing.
* Remove-from-interest action gated by a confirmation dialog.

API (`src/app/api/v1/interests/[id]/berths/...`)

* `GET /` - list endpoint returning `listBerthsForInterest` plus the parent
  interest's `eoiStatus` in `meta.eoiStatus` so the UI can decide whether to
  show the bypass control.
* `PATCH /[berthId]` - partial update of the junction row's flags + bypass
  fields. Server-side guard: rejects bypass writes when `eoiStatus !==
  'signed'` (defence in depth - never trust the UI to gate this).
* `DELETE /[berthId]` - calls `removeInterestBerth`.
* The existing POST stays unchanged. All routes wrapped with
  `withAuth(withPermission('interests', view|edit, ...))`. portId from ctx;
  cross-port reads/writes return 404 for enumeration prevention (§14.10).

Service changes (`src/lib/services/interest-berths.service.ts`)

* `upsertInterestBerth` now accepts `eoiBypassReason` (tri-state: omit = no
  change, non-empty = record, null = clear) and `eoiBypassedBy`. The bypass
  triple moves as a unit, with `eoi_bypassed_at` stamped server-side.
* `listBerthsForInterest` now returns berth detail (area, status, dimensions)
  alongside the junction row, typed as `InterestBerthWithDetails`.

Socket: added `interest:berthLinkUpdated` event for live UI refreshes.

Tests: 18 new integration tests in `tests/integration/api/interest-berths.test.ts`
covering happy paths, primary-demotion in same tx, bypass write/clear, the
"requires signed EOI" guard, cross-port 404s, missing-link 404s, empty-body
400, and viewer 403 through the permission gate.
2026-05-05 04:01:56 +02:00
Matt Ciaccio
15d4849030 feat(recommender): API endpoint + interest-detail panel + add-to-interest dialog 2026-05-05 03:05:22 +02:00
Matt Ciaccio
8699f81879 chore(style): codebase em-dash sweep + minor layout polish
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m18s
Build & Push Docker Images / build-and-push (push) Has been skipped
Replaces every em-dash and en-dash with regular ASCII hyphens
across comments, JSX strings, and dev-facing logs. Mostly cosmetic
but stops the inconsistent mix that crept in over the last few
months (some files used em-dashes in comments, others didn't,
some used both).

Bundles two small dashboard-layout tweaks that touch a couple of
already-modified files:
- (dashboard)/layout.tsx main padding goes from p-6 to pt-3 px-6
  pb-6 so page content sits closer to the topbar.
- Sidebar now receives the ports list it needs for the footer
  port switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:57:01 +02:00
Matt Ciaccio
57a099acc4 fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton
- Documents hub signer status now renders via a label map (`Pending`,
  `Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
  via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
  swaps the "Payment Method" free-text input for a `Select` of labelled
  options (`Bank transfer`, `Credit card`, …) so we never store
  `bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
  helper so values like `waiting_for_signatures` show as
  `Waiting For Signatures` (correctly title-cased) instead of being a
  lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
  renders any unknown outcome as a closed-state badge, preventing a
  closed interest from looking open just because its enum was added
  upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
  `userName` alongside `userId`; the client renders the resolved name
  instead of a 36-char UUID. Falls back to `'a teammate'` if the user
  row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
  with a server-resolved client/company name via a small `useQuery`,
  so users can confirm they picked the right billing entity before
  submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
  card skeleton during the server-component / initial-query window
  that previously flashed empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:01:35 +02:00
Matt Ciaccio
a767652d74 feat(sales-ux): triage signals, reminders, realtime toasts, mobile FAB
Sales-CRM workflow batch — closes audit recommendations #2, #3, #4, #6,
#7, #8, #9, #10, #13, #15. Skips #11 (My-pipeline filter — needs a real
assignee column on interests, defer until ownership model lands) and #12
(keyboard shortcuts — explicit user call).

  Interest list (the rep's main triage surface):

    - Last activity column replaces Created (sortable by
      dateLastContact). Postgres NULLs-last on DESC means
      never-contacted leads sort to the bottom — exactly the right
      triage default.
    - Comment-icon next to client name when notesCount > 0, with a
      tooltip showing the count. Cheap, glanceable signal that the
      lead has correspondence to peek at.
    - Urgency badges under stage when criteria fire: "Silent Nd"
      for mid-funnel interests with no contact in 7+ days,
      "EOI Nd" for EOIs awaiting signature 14+ days, "Deposit Nd"
      for eoi_signed interests with no deposit after 21 days.
      Pure derived — no extra fetch, computed from the dates the
      row already returns.
    - Bulk select checkbox column with bulk-archive (existing
      DataTable.bulkActions API; just wired with a confirm-dialog
      and a Promise.all fan-out).
    - Mobile FAB (+) for new interest, anchored above the bottom-tab
      bar with safe-area inset awareness.

    All four signals mirrored on the mobile InterestCard (comment
    icon, urgency badges, last-activity footer).

  Interest detail:

    - Reminder bell badge in the header showing pending/snoozed
      reminder count linked to the interest. Surfaced via
      getInterestById's new `activeReminderCount`.
    - "Latest note" teaser on the Overview tab — truncated 3-line
      preview of the most recent threaded note + relative time +
      "View all" link to the Notes tab. Saves a click for the
      common "what was discussed last?" peek.
    - Color-block swatches in InlineStagePicker dropdown (rounded-sm
      mini-bars in the stage's progressive saturation color, replacing
      the previous tiny dots). Reads as a visual scan instead of a
      list.

  Dashboard:

    - MyRemindersRail on the right sidebar above the existing
      AlertRail. Shows pending+snoozed reminders for the current
      user (overdue first), each with priority pill, relative due
      time, and click-through to the linked interest/client/berth.

  Berth detail:

    - BerthInterestPulse card at the top of the Overview tab,
      replacing the old "buried in tab" pattern. Shows up to 5
      active interests with avatar, stage pill, urgency badges, and
      last-activity. Mirrors the old Nuxt CRM's beloved "Interested
      Parties" panel but with the new triage signals.

  Realtime toasts:

    - New <RealtimeToasts /> mounted inside SocketProvider in the
      dashboard layout. Subscribes to interest:stageChanged,
      document:completed, document:signer:signed, and
      interest:outcomeSet — fires sonner toasts so reps watching any
      page learn about pipeline events without refreshing.

  Service layer:

    - listInterests: notesCount per row (left join + count + groupBy).
    - getInterestById: clientPrimaryPhone + clientPrimaryPhoneE164
      (for the Email/Call/WhatsApp buttons added last commit; phone
      pieces were missing), notesCount, recentNote, activeReminderCount.
    - sortColumn switch handles 'dateLastContact' explicitly; default
      stays 'updatedAt'.

tsc clean. vitest 835/835 pass. ESLint clean on every file touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 04:09:51 +02:00
Matt Ciaccio
76a7387dcc fix(ux): batch UX audit fixes across spine pages
Comprehensive audit findings rolled up into one pass.

Bugs:

  - dialog.tsx — sm-breakpoint centering classes (sm:left-[50%] /
    sm:top-[50%]) were being silently stripped by tailwind-merge because
    the base inset-0 + sm:inset-auto pair counted as a conflict. Replaced
    with explicit per-side utilities (top-0 right-0 bottom-0 left-0 +
    sm:right-auto sm:bottom-auto). Every Dialog instance now centers
    correctly on desktop. (Affected 16 dialog consumers.)

  - interest-documents-tab.tsx — useQuery shared the queryKey
    ['interests', interestId] with the parent InterestDetail's query but
    returned a different shape ({ data: ... } envelope vs unwrapped).
    They clobbered each other's cache on tab mount, degenerating the
    parent header to "Unknown Client" / "Open" briefly. Unified the
    queryFn shape so the cache stays consistent.

  - interest-tabs.tsx — milestone steps now derive done-state from
    PIPELINE_STAGES.indexOf(currentStage) >= step.advanceStage_idx as
    well as from the date stamp. Stage truth > date truth. Seeded /
    imported interests that arrived past `open` without per-step dates
    now correctly show their milestone steps as checked.

  - interest-detail.tsx — wires useMobileChrome so the mobile topbar
    shows the client name instead of the interest UUID.

  - interest-documents-tab.tsx — empty state restructured to a centered
    "No documents yet — Generate EOI" CTA card instead of a small
    primary button floating in the corner.

  - timeline/route.ts — synthesizes a "Created at <stage>" event when
    no audit-log rows exist for the interest, so the Activity tab
    isn't empty for seeded interests.

  - lead-source-chart.tsx — pie radii switched from fixed 90px/50px
    to "70%"/"40%" so the pie scales with the container instead of
    being clipped at narrow widths; reserved 40px for the legend.

Visual / clarity:

  - interest-detail-header.tsx — Won/Lost rendered as branded text
    buttons on desktop ("Mark won", "Close as lost") and icon-only on
    mobile via `hidden sm:inline`. Edit/Archive stay icon-only. Reopen
    promoted to a labeled button when the interest is closed. Added
    "Last contact Xd ago" to the meta row.

  - detail-header-strip.tsx — py-4 → py-3 (tighter strip).

  - interest-tabs.tsx — milestone cards: the next pending milestone
    gets a brand-blue ring + "NEXT" pill so the user can see at a
    glance which lifecycle to act on. Its primary action gets the
    filled button variant.

  - interest-tabs.tsx — Deposit milestone: invoice flow promoted to
    primary CTA ("Create deposit invoice"), manual stage advance
    demoted to a small text link ("Mark received manually"). Reflects
    the actual recommended path now that recordPayment auto-advances
    on payment.

  - inline-editable-field.tsx — pencil affordance shown faintly
    (opacity-20) at rest so users discover that fields are editable
    without having to hover-test every label. Lifts to opacity-60 on
    hover.

  - constants.ts — STAGE_SHORT_LABELS map for cramped contexts;
    pipeline-chart.tsx + pipeline-funnel-chart.tsx use them on mobile
    via useIsMobile, so the rotated 9-stage axis isn't a wall of
    overlap on a 393px screen.

  - client-pipeline-summary.tsx — StageStepper rebuilt as a single
    segmented progress bar instead of 9 micro-dots + connectors that
    rendered inconsistently at tight widths. Each stage is an equal
    slice that lights up as the interest reaches it; tooltips on hover
    give the full stage name. Also dropped a pre-existing dead `br`
    variable.

  - dashboard empty states — Lead Source, Revenue Breakdown, Pipeline
    Funnel, and Recent Activity now have helpful descriptions explaining
    what populates them, instead of bare "No interests in range".

  - use-paginated-query.ts — reuses `&` when the endpoint already has
    `?`, so callers like the documents hub don't generate
    `…?tab=eoi_queue&signatureOnly=true?page=1&limit=25` (which the API
    rejected as 400). Caught while testing the now-removed EOI route
    but applies broadly.

tsc clean. vitest 832/832 pass. eslint 0 errors (down from 1
pre-existing) on every file touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 01:24:15 +02:00
Matt Ciaccio
ba5fb6db5e feat(sales): EOI queue route + invoice→deposit auto-advance + won/lost outcomes
Three independent strengthenings of the sales spine that the prior coherence
sweep made it possible to do cleanly.

  1. EOI queue page

     - Sidebar entry under Documents → "EOI queue".
     - Route /[port]/documents/eoi renders DocumentsHub with the existing
       eoi_queue tab pre-selected (filters in-flight EOIs only).
     - .gitignore: tightened root-only `eoi/` ignore so the documents/eoi
       route is no longer silently excluded.

  2. Invoice ↔ deposit link

     - invoices.interestId (FK, ON DELETE SET NULL) + invoices.kind
       ('general' | 'deposit'). Indexed on (port_id, interest_id).
     - createInvoiceSchema requires interestId when kind === 'deposit';
       the service validates the linked interest belongs to the same port
       before insert.
     - recordPayment auto-advances pipelineStage to deposit_10pct (via
       advanceStageIfBehind) when a paid invoice is kind=deposit and has
       an interestId. No-op if the interest is already further along.
     - "Create deposit invoice" link added to the Deposit milestone on the
       interest detail. Links to /invoices/new?interestId=…&kind=deposit;
       the form prefills the billing entity from the linked interest's
       client and shows a context banner.

  3. Won / lost terminal outcomes

     - interests.outcome ('won' | 'lost_other_marina' | 'lost_unqualified'
       | 'lost_no_response' | 'cancelled') + outcomeReason text +
       outcomeAt timestamp. Indexed on (port_id, outcome).
     - setInterestOutcome / clearInterestOutcome services + POST/DELETE
       /api/v1/interests/:id/outcome endpoints (gated by change_stage
       permission). Setting an outcome moves the interest to `completed`
       in the same write; clearing reopens to `in_communication` (or a
       caller-specified stage).
     - Mark Won / Mark Lost icon buttons on the interest detail header,
       plus an outcome badge that replaces the stage pill once a terminal
       outcome is set, plus a Reopen button.
     - Funnel + dashboard math updated to exclude lost/cancelled outcomes
       from active calculations (KPIs.activeInterests, pipelineValueUsd,
       getPipelineCounts, computePipelineFunnel, getRevenueForecast).
       The funnel now also returns a `lost` summary so callers can
       surface leakage without polluting conversion percentages.

Schema changes shipped via 0019_lazy_vampiro.sql; applied to dev DB
manually via psql because drizzle-kit push hits a pre-existing zod
parsing issue on the companies index. Dev server may need a restart
to flush prepared-statement caches.

tsc clean. vitest 832/832 pass. ESLint clean on every file touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:01:33 +02:00
Matt Ciaccio
886119cbde refactor(sales): consolidate pipeline stages + wire EOI auto-advance
The 8→9 stage refresh from earlier today only updated constants.ts and the DB —
20 component/service files still hardcoded the old enum, leaving labels blank,
filter dropdowns wrong, kanban columns mismatched, and the analytics funnel
silently dropping new-stage rows. The platform also never advanced
pipelineStage on EOI lifecycle events: documents.service.ts wrote eoiStatus
but left the user-visible stage stuck.

This commit closes both gaps:

  1. Single source of truth in src/lib/constants.ts — adds STAGE_LABELS,
     STAGE_BADGE, STAGE_DOT, STAGE_WEIGHTS, STAGE_TRANSITIONS plus
     stageLabel / stageBadgeClass / stageDotClass / safeStage /
     canTransitionStage helpers. components/clients/pipeline-constants.ts
     becomes a re-export shim so existing imports keep working.

  2. 18 stale-enum surfaces migrated — interest list (table, card, filters,
     form, stage picker), pipeline board, client card, berth interests tab,
     portal client interests page, dashboard pipeline / funnel / revenue-
     forecast charts, settings pipeline_weights default, dashboard.service
     weights, analytics.service funnel stages, alert-rules stale-interest
     filter, interest-scoring stage rank.

  3. Documents tab wired into interest detail — replaced the placeholder in
     interest-tabs.tsx with InterestDocumentsTab + InterestFilesTab so the
     EOI launcher is back where salespeople work.

  4. Auto-advance — new advanceStageIfBehind() in interests.service.ts
     (forward-only, no-op if interest is already past the target). Called
     from documents.service.ts on send (→ eoi_sent), Documenso completed
     webhook (→ eoi_signed), and manual signed-EOI upload (→ eoi_signed).

  5. Transition guard — canTransitionStage() blocks egregious skips
     (e.g. completed → open, open → contract_signed). Enforced in
     changeInterestStage before the DB write.

Tests updated to reflect the 9-stage model. tsc clean, vitest 832/832,
ESLint clean on every file touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:33:53 +02:00
Matt Ciaccio
e8d61c91c4 feat(platform): residential module + admin UI + reliability fixes
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 1m2s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped
Residential platform
- New schema: residentialClients, residentialInterests (separate from
  marina/yacht clients) with migration 0010
- Service layer with CRUD + audit + sockets + per-port portal toggle
- v1 + public API routes (/api/v1/residential/*, /api/public/residential-inquiries)
- List + detail pages with inline editing for clients and interests
- Per-user residentialAccess toggle on userPortRoles (migration 0011)
- Permission keys: residential_clients, residential_interests
- Sidebar nav + role form integration
- Smoke spec covering page loads, UI create flow, public endpoint

Admin & shared UI
- Admin → Forms (form templates CRUD) with validators + service
- Notification preferences page (in-app + email per type)
- Email composition + accounts list + threads view
- Branded auth shell shared across CRM + portal auth surfaces
- Inline editing extended to yacht/company/interest detail pages
- InlineTagEditor + per-entity tags endpoints (yachts, companies)
- Notes service polymorphic across clients/interests/yachts/companies
- Client list columns: yachtCount + companyCount badges
- Reservation file-download via presigned URL (replaces stale <a href>)

Route handler refactor
- Extracted yachts/companies/berths reservation handlers to sibling
  handlers.ts files (Next.js 15 route.ts only allows specific exports)

Reliability fixes
- apiFetch double-stringify bug fixed across 13 components
  (apiFetch already JSON.stringifies its body; passing a stringified
  body produced double-encoded JSON which failed zod validation)
- SocketProvider gated behind useSyncExternalStore-based mount check
  to avoid useSession() SSR crashes under React 19 + Next 15
- apiFetch falls back to URL-pathname → port-id resolution when the
  Zustand store hasn't hydrated yet (fresh contexts, e2e tests)
- CRM invite flow (schema, service, route, email, dev script)
- Dashboard route → [portSlug]/dashboard/page.tsx + redirect
- Document the dev-server restart-after-migration gotcha in CLAUDE.md

Tests
- 5-case residential smoke spec
- Integration test updates for new service signatures

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:54:32 +02:00
67d7e6e3d5 Initial commit: Port Nimara CRM (Layers 0-4)
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00