c1daed199154a0b9cbd4c811206ca1dff56301a1
86 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 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>
|
|||
| 0ed03fcd7f |
feat(uat-batch): Group P — nested document subfolders phases 2/3
P56 from the 2026-05-21 plan. Foundation (phase 1) shipped in
|
|||
| 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>
|
|||
| 431375d794 |
feat(uat-batch): Groups D + E — wizard polish + supplemental-info history
D24 + D25 + E26 from the 2026-05-21 plan. All three shipped.
Shipped now:
D24 BulkAddBerthsWizard ft/m toggle. Step 2 header gets a small
monospaced ft/m button that flips the dimension entry unit
wizard-wide. Cell values stay as-typed; on submit a single
`inputToFt(v)` helper converts m→ft (1 m = 3.28084 ft) before
posting the canonical feet payload. Column headers update
Length/Width/Draft labels to reflect the active unit.
D25 BulkAddBerthsWizard dock-letter expansion. Replaced the
Select-of-A–E with a chip group + free-text "Other…" input.
Common letters (A-E) are quick-pick chips; reps can type any
uppercase letter sequence (AA, BB, F, …) for ports whose dock
layout extends past the five-letter shortlist. New
`handleGenerate` validation rejects empty / non-uppercase
inputs with a toast. Custom-input path uppercases + strips
non-letters as the rep types so the canonical
`^[A-Z]+\d+$` mooring regex always matches.
E26 Supplemental-info Regenerate / Resend / history.
Service: new `listTokensForInterest(portId, interestId)`
returns the latest 20 issuances with expired/consumed flags;
new `getTokenForResend(portId, interestId, tokenId)` snapshots
a specific token back into the issue-shape so the route can
re-email without minting a fresh token.
Route: GET lists the issuances (gated on `interests.view`);
POST accepts an optional `tokenId` for the Resend branch
(forces `sendEmail=true` since the rep clicked with intent)
and returns `resent: true/false` on the success payload.
UI: button card now shows three actions — Generate /
Regenerate link, Generate + email (or "New link + email"
when a usable token exists), and Resend current (only when
there's an active unconsumed unexpired token). Issuance
history list shows Active / Submitted / Expired per row.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 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>
|
|||
| 47c2ba9a99 |
feat(reports): client / berth / interest list-export PDF reports (phase B)
Extends the report exporter with three list-style report kinds —
clients, berths, interests. Each shares the BrandedReportDocument
layout + the new ReportTable primitive (zebra-striped rows,
proportional widths, no-break rows to keep records together across
page boundaries).
Data fetchers in `src/lib/services/list-report-data.service.ts`:
- resolveClientReportData: clients table joined to per-client
primary email + phone via DISTINCT-style subqueries (matches the
canonical listClients ordering: is_primary DESC, created_at DESC
per channel).
- resolveBerthReportData: berths table, default sort by mooring
number for printed familiarity.
- resolveInterestReportData: interests left-joined to clients +
primary berth, sort by updatedAt desc.
All three cap at 1 000 rows per export with a clear "Showing top N
of <total>" notice rendered when the cap is hit. Above that, the PDF
becomes unreadable (hundreds of pages); reps wanting larger exports
use CSV.
Route schema widened to a 4-arm discriminated union; the dispatch
switch in render-report.ts uses `satisfies` for compile-time variant
narrowing and a `_exhaustive: never` check at the bottom.
UI: each list page (BerthList, ClientList, InterestList) gains an
ExportListPdfButton next to the existing ColumnPicker. Permission-
gated client-side on reports.export; server route re-enforces.
Tests: 3 new render fixtures (1 per kind), all hit the same
%PDF-magic + byte-length assertions. Total render tests now 6/6;
full vitest sweep 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 0c6e7b72af |
feat(forms): migrate remaining native date inputs to <DatePicker> / <DateTimePicker>
Sweeps the last ~17 native `<Input type="date"|"datetime-local">`
call sites onto the shared `<DatePicker>` / `<DateTimePicker>`
primitives so date entry is uniform across the app (calendar popover
on desktop, native OS picker on mobile via the primitive's
viewport-aware fallback).
Three patterns handled:
1. Controlled value/onChange — direct swap to <DatePicker
value/onChange>:
audit-log-list.tsx (audit-from / audit-to filters)
reports/generate-report-form.tsx (date range)
scan/scan-shell.tsx (expense date)
reservations/reservation-detail.tsx (end-reservation dialog)
shared/filter-bar.tsx ('date' filter variant)
2. RHF `register('field')` pattern — wrapped in <Controller> with
field.value/field.onChange bridge. The picker's '' → undefined
normalisation kicks in via `field.onChange(v || undefined)`:
berths/berth-form.tsx (tenureStartDate + tenureEndDate)
reservations/berth-reserve-dialog.tsx (startDate)
companies/add-membership-dialog.tsx (startDate)
yachts/yacht-transfer-dialog.tsx (effectiveDate)
invoices/invoice-detail.tsx (paymentDate)
3. RHF + Date-typed schema — same Controller wrap, plus a
Date<->YYYY-MM-DD bridge in the render() since the zod schema
coerces these to Date:
expenses/expense-form-dialog.tsx (expenseDate)
companies/company-form.tsx (incorporationDate)
4. Datetime variants — swapped onto <DateTimePicker>:
interests/interest-contact-log-tab.tsx (occurredAt + followUpAt)
Skipped because they ARE picker primitives or internal date variants:
- ui/date-picker.tsx, ui/date-time-picker.tsx (the primitives)
- shared/inline-editable-field.tsx (the InlineEditableField date variant)
- dashboard/date-range-picker.tsx (its own popover with min/max gating
that doesn't map cleanly onto the shared primitive)
Removed now-unused Input imports from four files.
Verified: tsc clean, vitest 1448/1448.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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 `—` 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> |
|||
| 3999d4bbea |
feat(interests): explicit "Add berth" CTA on LinkedBerthsList
Previously reps could only add berths through the recommender panel below the list or by indirect side-effects (EOI generation). New button on the card header opens a searchable picker dialog backed by /api/v1/berths/options. - AddBerthDialog uses the existing Command primitive (cmdk) for the searchable list. Berths already linked to the interest are filtered out so the rep can't double-add. - "Specifically pitching" switch surfaces the same Under Offer consequence the per-row toggle does. Defaults off (interest is internal-only until the rep promotes it). - Mutation hits POST /api/v1/interests/[id]/berths with the new link's `isSpecificInterest` flag. is_in_eoi_bundle / is_primary stay at their server defaults — the rep flips them on the row after the link lands. Invalidates interest-berths + berth-recommendations caches so the row appears immediately and the recommender drops the just-added berth. - Dialog only mounts while open so picker state resets on each invocation (avoids set-state-in-effect re-hydration). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| a4e30ea16c |
feat(uat-batch-23): supplemental-info — separate Generate link + Send by email
The single-button "Request more info" conflated link generation with
email send. Once tokens became reusable until expiry (PR15), the
two-step UX makes more sense — reps often need to copy the link and
share it via WhatsApp / iMessage instead of letting SMTP route it.
- API: POST /supplemental-info-request now accepts an optional
`{ sendEmail?: boolean }` body (defaults true for back-compat).
Generate-only callers pass `{ sendEmail: false }`.
- UI: two buttons replace the single CTA — "Generate link" (always
generates, never emails) + "Send by email" (the original
full-blow behaviour). Re-clicking "Generate link" with a token
already issued mints a fresh one (labeled "Regenerate link").
- Email body copy: drop "can only be used once" since PR15 made the
link reusable until expiry.
tsc clean. 1419/1419 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 72d7803be5 |
feat(uat-batch-19): a11y th scopes + legend styling + i18n locale fixes
- Raw `<th>` cells gain `scope="col"` so SR users get proper column association: berth-interests-tab, bulk-add-berths-wizard, clients/bulk-hard-delete-dialog. shadcn `<TableHead>` migration would be cleaner but the scope attribute is the minimum-effort fix the queue's a11y entry asks for. - supplemental-info form `<legend>` elements styled with `mb-2 px-1 font-semibold` so they read as section headings rather than blending into the surrounding fieldset border (default browser legend rendering is barely visible). - payments-section: invalid `'en-EU'` BCP-47 locale → `undefined` to honour browser locale. - ui/calendar: literal `'default'` → `undefined` on the month dropdown formatter, same reason. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 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> |
|||
| 552b966903 |
feat(uat-batch-14): InterestDocumentsTab rename, custom-field tooltip, yacht Transfer surface
- InterestDocumentsTab section "Legal documents" renamed to
"Signature documents" so its scope is unambiguous. The section
holds Documenso envelopes (EOI / Reservation / Contract); generic
legal uploads belong in Attachments below.
- Custom-field admin form's "Sort Order" label now uses the
FieldLabel primitive with an explainer tooltip ("Lower numbers
render first... use to pin frequently-edited fields to the top").
First adoption of the FieldLabel primitive shipped in PR4.2.
- Yacht Ownership History tab gains a "Transfer ownership" button:
in the populated state as a header CTA (perm-gated by yachts.edit),
in the empty state as the EmptyState action. Reuses the existing
YachtTransferDialog from the header. Closes the "no way to enter/
change" UX gap without duplicating the transfer logic.
- Verified the existing row-owner rendering already uses OwnerLink,
so the row-click affordance was already in place.
tsc clean. 1419/1419 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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> |
|||
| 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> |
|||
| 51ca875665 |
feat(uat-batch-8): qualification rework — intent auto-confirm + derived-only + collapse-when-done
Three coordinated changes to the per-interest qualification checklist that collectively trim it from a noisy gate into an out-of-the-way audit log once the deal moves forward. - Auto-confirm `intent_confirmed` once `pipelineStage > qualified`. Signing an EOI (or later) is the strongest signal of intent; the checklist no longer requires a redundant explicit tick. Evidence string reads "Stage advanced past Qualified". - `dimensions` becomes derived-only — explicit ticks no longer override removed evidence. When the rep deletes a yacht link or clears desired dims, the row un-ticks immediately. Judgement-based criteria keep the OR semantic so a manual confirmation survives an evidence change. - Checklist auto-collapses when fully confirmed: header shows ✓ All confirmed (label · label) with a chevron; rep clicks to expand and inspect or untick. Forced-expanded whenever an item is still outstanding. ARIA-controlled. - `qualification.service` gains a `pipelineStage` column-select and threads it through `AutoCtx`; `DERIVED_ONLY_KEYS` Set sentinel drives the new merge semantic. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| c6dcf49e18 |
feat(uat-batch-7): Wave-2 polish — Open-in-Documents, berth label, residential, NotesList parity
- InterestEoiTab history link renamed "Open" → "Open in Documents" so the cross-section nav target is unambiguous. - DocumentDetail Interest link sub-text now shows the derived `berthLabel` (formatBerthRange of the in-EOI-bundle subset, falling back to primary, then all linked berths). The link no longer duplicates the Client name; falls back to clientName or "No berths linked" when no berths exist. - New /<port>/residential/page.tsx redirects to /residential/clients so the breadcrumb's Residential link works. - Residential interests list — whole row is now a Link target (was hidden behind a trailing "View" link); hover + border accent on the full row. - Expenses PageHeader description "Track and manage port expenses" → "Track and manage business expenses" (drop the redundant "port", same audit pattern flagged in the queue). - DropdownMenu base content capped at `max-h-96` (was the Radix available-height variable, which stretched menus edge-to-edge); the existing internal scroll handles overflow. - Yacht Overview Notes block: replaced the legacy single-field textarea with the threaded `<NotesList entityType="yachts">` for parity with clients/interests/companies. Legacy `yacht.notes` column stays in schema for EOI/contract merge-field path. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 301375a3c3 |
feat(uat-batch-6): external-EOI structured signatories + X/Y signed counter
Replace the freetext CSV signer-names field with a structured recipient editor (name / email / role per row). Service now persists each non-CC signatory as a `document_signers` row pre-stamped `status='signed'` so the document-detail "X / Y signed" badge counts correctly for manually-uploaded EOIs. - ExternalEoiInput gains a structured `signatories` field; legacy `signerNames` retained for back-compat. Role enum: `client | developer | rep | witness | cc`. - uploadExternallySignedEoi inserts `document_signers` rows for every non-CC entry inside the existing transaction. - documentEvents.completed event records both shapes for full audit fidelity. - POST /api/v1/interests/[id]/external-eoi parses the `signatories` JSON multipart field defensively; malformed payloads fall back to signerNames. - Dialog UI: per-row Name / Email / Role inputs with add / remove. Seeds from interest's clientName + clientPrimaryEmail via a signatoriesOverride/null pattern (React-Compiler safe — no setState-in-effect). tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 203f543e60 |
feat(uat-batch-5): UI polish — dialog width, chart centering, recommender pill, audit link, inbox reorder
Six surgical Wave-2-3 wins:
- UploadForSigningDialog: dialog widened to max-w-[1400px] w-[95vw] so
the place-fields step actually has room; recipient row converts from
fixed grid to flex (name flex-1, email flex-[2] for the longer
string, role w-40, delete shrink-0); invitation-message textarea
rows 3 → 6.
- ChartCard becomes flex-col with flex-1 + items-center on CardContent
so charts vertically center when neighbouring cards make the row
taller (e.g. Pipeline Value's full breakdown).
- Berth recommender pill: drops the "Tier {letter} · " prefix; shows
just the plain-English label ("Open" / "Fall-through" / "Active
interest" / "Late stage") as a Popover trigger that explains the
4-state ladder. HelpCircle icon makes the tooltip discoverable.
- Activity feed gains a "See all" link in the header pointing at
/<port>/admin/audit, permission-gated by `admin.view_audit_log`.
- Inbox section order swaps to Reminders above Alerts (rep-noted
priority); PageHeader title flips to "Reminders & Alerts". Section
ids, deep-link hashes, and localStorage open-state keys untouched.
- Inbox ReminderList (embedded mode only): "New Reminder" button now
shares the filter row (right-aligned via ml-auto) instead of
occupying its own dedicated row above the filters.
tsc clean. 1419/1419 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 52342ee45d |
feat(uat-batch-4): a11y form primitives + click-to-preview + EOI empty-state + lint guards
- FieldError primitive (role=alert, aria-live) — used by Wave 3 form-error UX work. - FieldLabel primitive (Label + Info-tooltip slot) — foundational for the platform-wide admin-settings tooltip audit. - ESLint guard against em-dash in user-facing JSX text inside src/components + src/app (warning, not error; 111 existing instances flagged for follow-up sweep). - FileGrid card body becomes click-to-preview button (was hidden under a kebab); aria-label per row; kebab keeps Download/Rename/Delete. - DocumentList: title cell on rows with signedFileId opens FilePreviewDialog; kebab gains Download action (was missing per UAT). Single FilePreviewDialog instance lifted to the parent. - DocumentList type extended with signedFileId. - EOI empty state: third ghost button "Mark signed without file" wired to existing MarkExternallySignedDialog (parity with reservation tab). - Watcher empty-state padding fix on document-detail. tsc clean. 1419/1419 vitest. lint clean on touched files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 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> |
|||
| 6cdb9af6b2 |
fix(uat-batch-2): external-EOI five-bug bundle (a/b/c/d) + presign filename override
Tackles the linked B4 #5 findings on the external-EOI flow. Item (e) [Edit metadata affordance per row] is deferred to a later wave so it can share infra with the broader signing-flow rework. - (a) lying toast: uploadExternallySignedEoi now returns { stageChanged, newStage }. Client toasts conditionally so a Reservation+ deal that uploads paper-signing evidence no longer claims the stage advanced. - (b) View downloads instead of previewing: SignedPdfActions takes an onView callback; InterestEoiTab lifts a single FilePreviewDialog and passes the callback down. Click-View opens the in-app preview rather than the presigned URL (which the storage backend served as attachment). - (c) UUID filename on download: getDownloadUrl now passes the canonical filename through presignDownloadUrl; S3 backend adds a response-content-disposition override (filename + UTF-8 filename*) to the presign. Filesystem backend already passed it through. - (d) Discarded dateEoiSigned: external-eoi service splits document- metadata writes (always — dateEoiSigned, eoiStatus='signed') from stage advance (gated on past-EOI). Also fires evaluateRule('eoi_signed') so berth-rules stay in sync when an EOI is filed manually. - Default title for external-EOI dialog now derives "External EOI — <Client> — <berth range> — <date>" via the existing formatBerthRange helper; rep can override. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 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> |
|||
| 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>
|
|||
| f938847ed9 |
feat(post-audit): Phase 5 partial (4/8 templates) + 7.1 editor scaffold + per-entity reminder buttons
Phase 5 — luxury-port email tone (4 of 8 templates):
- portal-auth.tsx — activation + reset: "It's our pleasure to invite
you to the {portName} client portal — your private space to review
your berth, manage signed documents, and stay in touch with your
sales liaison", sign-off "With warm regards, The {portName} Team",
subjects "Welcome to {portName} — activate your client portal" /
"Reset your {portName} portal password".
- inquiry-client-confirmation.tsx — "We've noted your enquiry, and a
member of our team will be in touch shortly through your preferred
channel", "should anything come to mind in the meantime", sign-off
"With warm regards, The {portName} Sales Team".
- notification-digest.tsx — "Your {portName} update" header, "Here's
what's waiting for you", "With warm regards, The {portName} Team".
- document-signing.tsx — all 4 sign-offs ("Dear X, ... Thank you, The
{portName} team") rewritten to "With warm regards, The {portName} Team"
with capitalised Team for consistency.
- Voice captured from old-CRM Nuxt repo
(/Users/matt/Repos/Port Nimara/Port Nimara Client Portal/client-portal/
server/utils/signature-notifications.ts) which already used "Dear",
"Best regards", and collective sign-offs.
Remaining 4 templates (admin-email-change, crm-invite,
inquiry-sales-notification, residential-inquiry) + cross-port snapshot
tests queued as follow-up.
Phase 7.1 — PDF editor scaffold:
- New admin route /admin/templates/[id]/editor/page.tsx wired to a
client-side <TemplateEditor>.
- Renders page 1 via react-pdf (worker URL pattern mirrors
components/files/pdf-viewer.tsx); click-to-place markers in percent
coordinates so a future page-size swap doesn't shift placements.
- Token picker over VALID_MERGE_TOKENS (sorted).
- Save persists overlayPositions via PATCH against the existing
document_templates row; validator accepts the new field via
fieldMapSchema from lib/templates/field-map.ts (no migration needed
— overlay_positions JSONB column already exists).
- Outer/inner-body split + key-by-templateId remount avoids the
in-render setState antipattern when seeding from server data.
- Add + delete markers supported. Multi-page, drag, resize, preview,
new-PDF upload all defer to 7.2.
Per-entity polish:
- [+ Reminder] button on yacht / client / interest detail headers,
threading defaultYachtId / defaultClientId / defaultInterestId so the
ReminderForm opens with the entity pre-linked.
- [EOI] badge on yacht detail header when yacht.source === 'eoi-generated'
(mirrors the contacts-editor pattern shipped in
|
|||
| 503207ef68 |
feat(post-audit): Phase 4 polish + Phase 2 wiring + Phase 6 cron + CLAUDE.md
Three of the master plan's "suggested execution order" items shipped this session; Phase 3b (EOI dialog overrides) deferred — estimate exceeded the remaining session time. - Phase 4 polish: yachtId field on <ReminderForm> via the existing YachtPicker, Ship-icon subtitle on <ReminderCard>, listReminders filter by yachtId, getReminder joins the yacht relation. - Phase 2 risk-signal data wiring: getInterestById derives the 3 dates (dateDocumentDeclined / dateReservationCancelled / dateBerthSoldToOther) from document_events / berth_reservations / cross-interest interest_berths in parallel — chosen over new schema columns to keep the master plan's "no new tables" promise. Threaded through to DealPulseChip. - Phase 6 cron + UI: src/jobs/processors/imap-bounce-poller.ts polls the configured IMAP mailbox (IMAP_* env), matches NDRs to recent document_sends rows via recipient + 7-day window, idempotent via bounceDetectedAt, fires email_bounced notifications on hard/soft (skips OOO). State persisted to system_settings.bounce_poller_state. Wired into maintenance queue at */15 * * * *. Admin /admin/sends page surfaces the bounce badge + reason inline. - CLAUDE.md: trimmed 27KB → ~19.5KB (~28% smaller bytes). Prose-heavy Documenso webhook / v1-v2 routing / Document folders sections rewritten as scannable bullets. Added a new "Working in this repo — skills, MCPs, agents" section promoting brainstorming/TDD/debugging/frontend-design skills, Context7/Playwright/Serena MCPs, and the Explore/feature-dev agents. Documented Phase 2 derivation choice in the data-model section. Quality gates: 1374/1374 vitest pass, tsc --noEmit clean, lint 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 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>
|
|||
| 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>
|
|||
| d2804de0d1 |
fix(ux): inline yacht-prereq picker + deprioritize country in client form
F23: when the rep tries to leave the Enquiry stage on an interest with no yacht linked, the stage popover now switches into an inline yacht-picker view (filtered to the client's own yachts when known). On submit it PATCHes interest.yachtId then chains the stage move, so the prereq fix and the advance happen in one flow instead of the rep bouncing to the validation error toast. F24: Country moved out of the Basic Information section (next to Full Name *) into Source & Preferences alongside Timezone — country is timezone-hint material, not first-line identity data. Quick-path for a new client is now just name + contact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 84468386d9 |
fix(ux): T4 polish wave — empty-contact filter, redirect-on-create, friendly stage errors
F19: client form drops empty-value contacts on submit; auto-promotes first remaining row to primary if none flagged. F20: new-interest dialog redirects to the detail page on create instead of bouncing back to the list. F21: stage-transition validation errors render with STAGE_LABELS — "Yacht is required before leaving the Enquiry stage." (was "yachtId is required before leaving stage=enquiry"). F22: blocked-stage marker swapped from the ⚑ unicode glyph to a Lucide AlertTriangle with aria-label. F25: documents-hub folder selection moves to ?folder=<id> querystring so deep-link / browser-back / refresh round-trip the current folder. F26: reopen-outcome action now toasts "Outcome cleared — interest is open again." F27: stage PATCH where target === current short-circuits to a no-op return; downstream callers don't see a phantom stage_change audit row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 3e78c2d4ab |
fix(F17 ext): apply DetailNotFound to clients/yachts/companies/berths
Refactored the interest-detail 404 pattern into a reusable `<DetailNotFound>` component and applied it to the four other entity detail pages. Pre-fix, navigating to a wrong-port or stale entity URL silently rendered the layout shell with empty tabs on: - /[portSlug]/clients/[id] - /[portSlug]/yachts/[id] - /[portSlug]/companies/[id] - /[portSlug]/berths/[id] All four now route a 404/403 response into an explicit "<Entity> not found" / "No access" EmptyState with a back-to-list CTA, and the TanStack Query retry policy short-circuits 404/403s so the empty state appears immediately. 1373/1373 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 608641c23b |
fix(T3): inline tag create + explicit 404 on interest detail
F16 — InlineTagEditor: inline "Create new tag" affordance The popover now has a search input at the top. Typing a name that doesn't match any existing tag surfaces a "Create new tag: <name>" action that POSTs /api/v1/tags then attaches the new id to the entity. Reps no longer need to context-switch to Admin → Tags to create the first chip. Enter on the input also triggers create-and-attach. F17 — Interest detail page: explicit not-found state Pre-fix, navigating to /port-X/interests/<port-Y-id> 404'd at the API but the UI silently rendered the list shell with empty tabs. Cross- port URL pastes now show an EmptyState with title "Interest not found" + a "Back to interests" CTA. 403 (no access in this port) gets its own copy. TanStack Query is told not to retry 404/403s so the empty state appears immediately. 1373/1373 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| e7e498dedd |
fix(T3): copy + entry points + recommender alias
Batch of small fixes from the post-audit plan:
F11 — "Mark as won" dialog copy
Was: "This will move the interest to Completed and stamp the outcome."
Completed was retired in the 7-stage refactor; copy now reads
"marks Won; stage stays where it is" with a parallel Lost variant.
F13 — Bulk-add berths wizard had no UI entry point
Page existed at /[portSlug]/admin/berths/bulk-add but nothing linked
to it. Added a "Bulk add" button on the Berths list toolbar, gated
on `berths.import`. Also fixed the API route's permission key
(was `berths.create`, a phantom — switched to `berths.import` to
match seed-permissions).
F14 — Audit Log nav entry
Sidebar Admin section now lists "Audit Log" → /admin/audit, gated
by the adminRequired group rule.
F18 — Recommender `limit` param ignored
POST /interests/[id]/recommend-berths now accepts `limit` as an
alias for `topN`. Audit sent `{limit:3}` and silently got 8 rows
back; both names now resolve.
Tests still green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 4182652d49 |
feat(externally-signed): mark contract/reservation as signed without file
Step 4 second slice. Adds the "Mark as signed without file" action to
contract + reservation tabs per PRE-DEPLOY-PLAN § 1.5.14.
Service: `markExternallySigned(interestId, portId, docType, reason)`
flips the relevant doc-status column ('contract_doc_status' /
'reservation_doc_status' / 'eoi_doc_status') to 'signed', writes an
audit log entry with `metadata.type='externally_signed'` capturing
the optional reason, and fires the appropriate berth-rule trigger
(eoi_signed / contract_signed) so downstream automation (berth
status flips, notifications) treats it identically to a Documenso-
signed completion.
Route: POST /api/v1/interests/[id]/mark-externally-signed gated on
interests.edit. Validates docType against the canonical 3-value enum.
UI: <MarkExternallySignedDialog> AlertDialog with optional reason
textarea + per-docType copy. Wired into EmptyContractState and
EmptyReservationState empty-state buttons. The action sits alongside
"Upload draft for signing" and "Upload paper-signed copy" as a third
option for reps whose canonical paper lives elsewhere.
EOI not yet wired into a UI surface — the eoi flow already has a
full upload pipeline. Service supports it for completeness.
Followup: quick brochure/PDF download buttons + per-user reminder
digest schedule still pending in Step 4 backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 905852b8a5 |
feat(permissions): carve out dedicated payments resource
Payments (deposit / balance / refund records on an interest) used to
share `invoices.record_payment`, which forces a port that doesn't
issue invoices at all to still navigate the invoicing permission
group to grant its sales reps payment-recording rights. Splitting
the resource lets admins gate the two surfaces independently.
The new resource has three actions:
- view — gates the UI affordance (API reads still go through
`interests.view`)
- record — POST / PATCH a payment
- delete — DELETE a payment record
Seed maps updated for all six system roles; existing role rows +
per-user permission overrides are backfilled by migration 0064 so
upgrades don't silently lose access. Two call sites (POST /interests/
[id]/payments, PATCH /payments/[id]) → payments.record; one
(DELETE /payments/[id]) → payments.delete. The PermissionGates on the
payments-section UI swap to the new keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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>
|
|||
| 7bf587de90 |
feat(documenso-phase-4): recipient configurator + field placement UI
Phase 4 lands the visual half of the Documenso build — the upload-
for-signing dialog the Contract + Reservation tabs hand off to. Four
files of new code; the existing tab placeholders point at it.
Files added:
- lib/services/document-field-detector.ts — Phase 4c auto-detect
scanner. Uses pdfjs-dist to extract per-page text + positions, then
matches anchor patterns (Signature, Date, Initials, Email, Name,
underscore-runs) and produces percent-coordinate DetectedField
rows. Recipient label inference walks ±100pt of each match for
Buyer/Seller/Client/Witness/Notary keywords. Returns [] when the
PDF is image-only; UI falls back to manual placement without an
error. 6 unit tests pin the matching + coordinate math.
- app/api/v1/documents/auto-detect-fields/route.ts — multipart POST
endpoint that delegates to detectFields(). Permission-gated by
documents.send_for_signing.
- app/api/v1/documents/signing-defaults/route.ts — GET endpoint that
surfaces just the per-port developer + approver display name/email
+ sendMode flag. No secrets exposed; lets the dialog prefill the
recipient configurator without an admin-scoped settings read.
- components/documents/upload-for-signing-dialog.tsx — the Phase 4
UI. Three-step state machine inside a single Dialog:
1. select-file: drop/click PDF picker + title input
2. configure-recipients: client + developer + approver prefilled,
rep can add/remove/reorder + change role (SIGNER/APPROVER/CC)
3. place-fields: react-pdf renders the source PDF; auto-detect
runs in the background on file load and seeds the overlay;
rep places, drags, resizes, deletes, reassigns fields via the
palette + side panel. Native DOM drag (no dnd-kit dependency
added — the coordinate math stays obvious).
Send fires POST /api/v1/interests/[id]/upload-for-signing (Phase 3
service); success toast reflects port sendMode (auto fires the
invite immediately, manual leaves it for the rep).
Files modified:
- components/interests/interest-contract-tab.tsx + reservation-tab.tsx:
swap the ComingSoonDialog placeholder for the real
UploadForSigningDialog with the matching documentType prop. The
placeholder ComingSoonDialog helper is deleted from both.
- scripts/tsc-staged.mjs: pull src/types/**/*.d.ts into the temp
staged-only tsconfig so side-effect CSS imports (e.g.
react-pdf/dist/Page/AnnotationLayer.css) resolve via the existing
declare-module shim. Without this fix the staged compile reports
TS2882 even though the full tsc --noEmit pass passes.
Design choices noted in code comments:
- Native drag over dnd-kit: the field overlay's percent-based
coordinate math is short enough that adding a drag library adds
complexity without saving lines.
- Auto-detect on file-load (not on demand): runs immediately so the
rep doesn't have to click a second button — empty result drops
back to manual placement silently.
- Per-recipient color swatches indexed by signingOrder.
- Recipient seed via useMemo + user-event handler instead of
useEffect → setRecipients (Wave 3 set-state-in-effect avoidance).
Server-side, Phase 3 plumbing handles the rest: tenant guard, magic-
byte verify, Documenso round-trip with per-port v1/v2 routing,
recipient signingToken capture for Phase 2 webhook cascade, auto-
send when port.sendMode === 'auto'.
Tests: 1334 → 1340 ✅ (6 new for the detector); tsc clean.
Deferred polish (Phase 6):
- Per-field metadata side panel for DROPDOWN/RADIO option lists
- Pinch-zoom + zoom-out controls on the field-placement canvas
- Recipient drag-reorder via dnd-kit
- Required toggle per field
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| ebdd8408bf |
fix(audit-wave-11): dossier sweep — error-ux + webhook + storage + search + maintainability
Final pass over the unaddressed AUDIT-2026-05-12 dossiers, taking the
tractable Critical/High items from each:
error-ux-auditor (5 items)
- C2: 17 toast.error(err.message) sites swept to toastError(err, …) so
every user-visible failure carries a copy-paste Reference ID
- C3: apiFetch synthesizes a client-side correlation id when a 5xx
comes back with a non-JSON body (reverse-proxy HTML pages); message
becomes "The server is unreachable. Please try again." with code
UPSTREAM_UNREACHABLE
- C4: checkRateLimit fails OPEN when Redis is unavailable so an outage
no longer 500s login + portal sign-in; logged at warn so monitoring
catches it
- H2: StorageTimeoutError (name='TimeoutError') replaces the plain
Error throw in s3.ts withTimeout — error-classifier hints fire now
- H5: errorResponse() adopted across /api/storage/[token],
/api/public/website-inquiries, and the Documenso webhook body (drops
the "Invalid secret" reconnaissance string)
outbound-webhook-auditor (5 items)
- C1: signature is now HMAC(secret, `${ts}.${body}`) with the
timestamp surfaced as X-Webhook-Timestamp so receivers can reject
replays outside a freshness window
- C3: dead-letter with reason missing_signing_secret when secret is
null (defence-in-depth against DB tampering / future migration
mistakes)
- H2: webhooks queue bumped to maxAttempts=8 with 30 s base
exponential backoff so a 30 s receiver blip during a deploy no
longer dead-letters every in-flight event; per-queue
backoffDelayMs added to QUEUE_CONFIGS
- M1: SSRF denylist gains Oracle Cloud metadata 192.0.0.192
- M2: dispatch-time https:// assertion before fetch, so a bad DB edit
can't slip plaintext through
storage-pathing-auditor (2 items)
- H1: berth-PDF presigned-upload keys now `${portSlug}/berths/…/…`
with portSlug threaded into backend.presignUpload — engages the
filesystem-proxy port-binding `p` token verifier
- H2: presignDownloadUrl auto-derives portSlug from the key's first
segment when callers don't pass it, so all 8 download sites engage
the `p`-token guard without per-site plumbing
search-auditor (1 item)
- H3: removed dead void wantEmail; void wantPhone; pair plus the
unused looksLikeEmail helper — the bucket-reorder it was scaffolded
for was never wired
maintainability-auditor (1 item)
- M2: swept seven abandoned `void <symbol>` markers and their dead
imports across clients/bulk, interests/bulk, admin/email-templates,
admin/website-submissions, alert-rules, and notes.service
Deferred to future work (substantial refactors, schema migrations, or
multi-file UI work):
- error-ux M3-M8 (global-error.tsx, per-route loading.tsx coverage,
ErrorBanner component, /api/ready route, worker DLQ admin surface)
- maintainability C1-C4 (documents/search/notes service splits,
interest-tabs split — multi-hour refactors)
- currency C1-H5 (mixed-currency dashboard aggregation, FX history
table, rounding policy) — wait for second non-USD port
- outbound-webhook C2 (deliveries reaper job), H1 (DNS-rebind TOCTOU
with undici Agent), H3 (circuit-breaker), H5 (presigned-post-policy)
- storage-pathing C2 (orphan reaper), H3-H5 (streaming + content-type
binding)
Tests: 1315/1315 vitest ✅ ; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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> |
|||
| 689a114aba |
fix(audit-wave-9): copy/terminology sweep (copy-auditor)
Address the highest-impact items from the copy-auditor's CRITICAL +
HIGH + MEDIUM bands:
**C2 portal raw-status leak**
- Drop the staff-only `leadCategory` chip from the portal interests
page entirely. Privacy + optics: clients should never see "hot lead"
in their own portal. `eoiStatus` was already wrapped in
`portalSigningLabel`; only the categorical chip remained.
**C3 signing-status label drift**
- Add `src/lib/labels/document-status.ts` as the single source of
truth for the {draft, sent, partially_signed, completed, expired,
cancelled} lifecycle: labels (CRM + portal variants), StatusPill
variant, and the "active / in-flight" set.
- Wire it into interest-eoi-tab, interest-contract-tab,
interest-reservation-tab — they previously redefined identical
STATUS_LABELS / ACTIVE_STATUSES blocks per-file.
**H1 + M3 verbiage codemod**
- `Save Changes` → `Save changes` (sentence case, matches the
surrounding admin/CRM pattern).
- `Saving...` (ASCII three dots) → `Saving…` (Unicode ellipsis).
Matches the project's UTF-8-elsewhere convention and reads
correctly via screen-readers.
**M1 envelope jargon → signing request**
- smart-archive-dialog: "Leave envelope pending" → "Leave signing
request pending"; "Void the signing envelope" → "Cancel the signing
request"; section header updated to match.
- document-detail: "voids the signing envelope" → "cancels the signing
request".
- bulk-archive-wizard: "leave invoices/signing envelopes alone" →
"leave invoices/signing requests alone".
- Documenso admin page intentionally keeps `envelope` (dev/integration
vocabulary).
**M5 Hot Lead casing**
- Normalize `Hot Lead` / `General Interest` / `Specific Qualified` to
sentence case in `constants.ts` LABEL_OVERRIDES and all per-file
lead-category maps so the CRM trend (sentence case) is consistent.
**C1 surface-level rename**
- "Linked prospect (optional)" → "Linked interest (optional)" on the
berth status-change dialog.
- "Deal Documents" tab → "Interest Documents" (URL/route kept as
`/deal-documents` to avoid breaking deep links; rename deferred).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 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> |
|||
| 4329db7fc3 |
fix(compiler): React Compiler safety triage — 5 categories cleared
Cleared 4 rule buckets (37 violations, including 5 real bugs) and
silenced 1 informational bucket from the Next 16 / react-hooks v7
upgrade. Cleared rules promoted from `warn` back to `error` so new
regressions block CI.
Real bug fixes:
- `interest-contact-log-tab.tsx`: `useMemo` used for side effects
(5 setState calls inside a memo body); converted to `useEffect`.
- `PieChart.tsx`: cumulative `let angle` mutation in a render-phase
`map`; converted to `reduce` so the slice array is built without
re-assignment.
- `documents-hub.tsx`: `useMemo(() => ({ count: 0 }))` used as a
mutable drag counter; converted to `useRef`.
- `notes-list.tsx`: `Date.now()` read during render for note-edit
countdown (impure) → pinned to a `now` state ticked every 30s.
- `onboarding-checklist.tsx` / `user-profile.tsx` /
`user-settings.tsx`: `useEffect(() => void load(), [])` with the
`load` function declared AFTER the effect — relied on hoisting,
trips Compiler's "access before declared" rule. Declared inside
the effect.
Pattern fixes (intentional cache-via-ref → state or layout-effect):
- 6 `ref.current = x` writes during render moved into layout
effects (`use-realtime-invalidation`, `settings-form-card`,
`inbox`).
- 3 `ref.current` reads during render (search totals cache,
scanner file ref) rewritten to backed-by-state.
- `use-is-mobile.ts` rewritten on `useSyncExternalStore` to avoid
the SSR-then-rehydrate setState dance.
- `use-notifications.ts` rewritten to write socket pushes directly
into the React Query cache via `setQueryData`, removing a local
state mirror.
Rule config (`eslint.config.mjs`):
- `react-hooks/purity` → error (was warn, cleared)
- `react-hooks/set-state-in-render` → error (was warn, cleared)
- `react-hooks/immutability` → error (was warn, cleared)
- `react-hooks/refs` → error (was warn, cleared)
- `react-hooks/incompatible-library` → off (informational only)
- `react-hooks/set-state-in-effect` → warn (51 remaining, all the
useEffect→fetch→setState data-fetch pattern; migration to
useQuery tracked in BACKLOG)
Verified: tsc clean, eslint 0 errors / 69 warnings (down from 105),
vitest 1315/1315, next build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 0ab96d74a8 |
feat(deps): Tailwind 3 → 4 + swap tailwindcss-animate for tw-animate-css
Ran the official @tailwindcss/upgrade tool: - tailwind.config.ts → @theme directive in globals.css - @tailwind base/components/utilities → @import 'tailwindcss' - postcss.config switched from tailwindcss + autoprefixer to @tailwindcss/postcss (autoprefixer baked in) - focus-visible:outline-none → focus-visible:outline-hidden (the v3 utility was a footgun — outline still showed in forced-colors mode) Reverted the migration tool's over-zealous variant="outline" → variant="outline-solid" rename on CVA prop values; that rename was meant for the Tailwind `outline:` utility, not our Button/Badge component variants. Swapped tailwindcss-animate (v3-style JS plugin) for tw-animate-css (v4-native @import). Same utility surface (animate-spin, animate-in, etc.), one fewer JS plugin in the bundle. Fixed the upgrade tool's malformed dark variant (@custom-variant dark (&:is(class *)) — `class` was being parsed as a tag) to canonical &:where(.dark, .dark *). Verified: tsc 0 errors, eslint 0 errors (16 pre-existing warnings), vitest 1315/1315, next build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| acf878f997 |
feat(deps): bump zod 3→4 + @hookform/resolvers 3→5
Resolved 65 type errors across the codebase via these v4 migration
patterns:
- `ZodError.errors` renamed to `ZodError.issues` (4 call sites in auth
routes + central error handler).
- `z.record(value)` now requires explicit key type: `z.record(z.string(),
value)`. Updated 7 sites across templates / forms / saved-views /
website-inquiries.
- `.refine(check, msgFn)` second-arg shape changed — now requires an
`{ error: (issue) => ... }` object form. Updated
`mergeFieldsSchema` in document-templates validator.
- `.transform(...).default(...)` chains: v4 enforces default value type
matches transform OUTPUT. Reordered to `.default(...).transform(...)`
in list-query / company-memberships handlers.
- `z.coerce.*()` INPUT type widened to `unknown` in v4. Service signatures
using `z.input<typeof schema>` (kept for caller flexibility around
defaults) now re-parse via `schema.parse(data)` to recover the
post-coercion shape Drizzle needs. Done in berth-reservations service.
Invoice service narrows `lineItems` locally with a typed cast since
re-parsing would double-validate.
- `.optional().transform(...)` no longer propagates the optional marker
through v4's new ZodPipe. Moved `.optional()` to the END of chain in
`optionalDesiredDimSchema` (interests) and documents list query
(folderId, signatureOnly).
- ZodIssue subtype shapes simplified: `received` removed from
invalid_type, `type` renamed to `origin` on too_small. Test fixtures
updated.
- @hookform/resolvers v5 splits Resolver into 3-generic form (Input,
Context, Output). useForm calls in 6 forms (client, yacht, berth,
interest, expense, invoices-new-page) now pass explicit generics:
`useForm<z.input<typeof schema>, unknown, z.infer<typeof schema>>`.
Verified: tsc clean (0 errors), vitest 1293/1293 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| d3960af340 |
feat: warm-up deps — ts-reset, web-vitals, RHF devtool, query-broadcast
Four low-risk adds before the Zod 4 / drizzle-zod headliner: - @total-typescript/ts-reset: tightens TS stdlib types globally (JSON.parse → unknown, fetch().json() → unknown, .filter(Boolean) narrows, Set literals respect typed Set targets). Caught 179 latent type errors; fixed all production sites (8 files) and added `any` cast escape hatch in test files (ESLint exemption scoped to tests/). - web-vitals + /api/v1/internal/vitals endpoint + WebVitalsReporter client component: establishes Core Web Vitals baseline (LCP/INP/CLS/ FCP/TTFB) via navigator.sendBeacon. Required before optimisation work. - @hookform/devtools + FormDevtool wrapper: dev-only RHF state inspector, lazy-loaded via next/dynamic so the chunk is excluded from prod bundles entirely. - @tanstack/query-broadcast-client-experimental: cross-tab cache sync via BroadcastChannel — wired in query-provider.tsx, 1-liner. Audit doc updated with sections 35 + 36 (PDF stack overhaul + comprehensive second-pass package sweep) covering ~20 package adoption candidates and 4-5 deprecation candidates. Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 4b9743a594 |
audit: 33-agent comprehensive audit + critical fixes
Full team audit run, all reports verbatim in docs/AUDIT-2026-05-12.md (5900+ lines, 30+ critical findings). Already-fixed this commit: - permission-overrides PUT: self-target block + RolePermissions allow-list + cross-tenant guard - /api/auth/resolve-identifier: rate-limit + synthetic miss-email kill enumeration - admin email-change: rotates account.accountId + revokes sessions - middleware: token-gated email confirm/cancel routes whitelisted - NAV_CATALOG: 10 dead-link sweeps to existing /admin/<x> targets Feature work landing same commit: optional username sign-in (migration 0054), per-user permission overrides (0055) with three-state matrix tabbed inside UserForm, user disable button, role + outcome + stage label normalisation across the platform, admin email-change with auto-notification template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 0ab7055cf1 |
feat(dashboard): local-time greeting + timezone-drift banner
Greeting - The "Good morning / afternoon / evening, Matt" line now derives from the browser's local time, computed inside a useEffect so the rendered HTML can't lock to the server's clock during hydration. Until the effect fires, the header reads "Welcome" — a neutral phrase that's correct at every hour and never produces a hydration warning. The phrase re-evaluates hourly so a rep leaving the dashboard open across a boundary (5am, noon, 6pm) doesn't keep stale text on screen. Timezone-drift banner - New <TimezoneDriftBanner> on the dashboard surfaces when the browser's resolved timezone (Intl.DateTimeFormat().resolvedOptions().timeZone, which follows the OS — and the OS usually follows physical location) doesn't match the user's stored CRM preference. The rep gets a one-tap "Update to Tokyo" button and a dismiss × that's sticky per browser via localStorage. - Why a banner rather than auto-update: the stored timezone drives reminder firing time, daily-digest delivery, and due-date rendering. Silently pinning it to a transient travel location would shift their reminder schedule underfoot. The banner gives them control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 04a594963f |
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces - New compact mooring-chip header (colored plate + status pill, dock-label in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack - Berth list gains a "Latest deal stage" column showing the most-advanced pipeline stage of any active linked interest (server-aggregated, ranks by PIPELINE_STAGES index) - "Linked prospect" Select on the status-change dialog rebuilt as a Command combobox: search, recent-first sort, stage-coloured pills Pipeline UX - Reverting an interest to Open with linked berths now prompts: keep the links, unlink and reset, or cancel. Silent when no berths are linked - Activity feed + entity-activity feed normalise enum field values via STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as "10% Deposit → Contract Sent" EOI generate dialog - Inline-editable rows for client name, nationality (country combobox), and yacht name — pencil affordance saves directly via clients/yachts PATCH - Replaces the single "Edit on client's page" link with two contextual links framed by short copy explaining what's inline vs what needs the canonical page - Backend EoiContext now includes client.id + yacht.id so the dialog can PATCH without an extra round-trip Company form - New "Connections" section lets the rep attach members (clients) and yachts during create. Yacht attach uses the existing transfer endpoint so audit log + ownership history capture the change - Inline "+ New client" / "+ New yacht" buttons open the canonical forms stacked over the company sheet - After save, the form chains to a yacht pull-in prompt (if any attached client owns yachts not yet linked) and an optional "Create interest" step pre-filled with the first attached client Admin - /admin landing gains a searchable index — typed query flattens groups into a result list matching label + description + group title - "Documenso & EOI" card relabelled to "EOI signing service" (consistent with the user-facing language rename from round 1) Measurement units (migration 0053) - interests gains desired_*_m columns + desired_*_unit discriminators so the rep's literal entry (ft OR m) is preserved verbatim instead of being reconstructed from a single canonical column on every render - yachts + berths gain matching *_unit columns alongside their existing ft + m pairs; defaults to 'ft' so legacy rows still render normally - Interest form POST/PATCH now sends both ft + m + unit; computed m is derived from the ft canonical to keep the recommender SQL unchanged Misc - Active-deals tile + topbar type their Link href as `Route` instead of `any` - Unused REPORT_TYPE_LABELS const dropped from generate-report-form - Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated to include the new id + unit fields on the EoiContext / Berth shapes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |