2026-05-05 03:05:22 +02:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useMemo } from 'react';
|
|
|
|
|
import Link from 'next/link';
|
|
|
|
|
import { useParams } from 'next/navigation';
|
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
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>
2026-05-21 17:28:20 +02:00
|
|
|
import {
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
Filter,
|
|
|
|
|
Flame,
|
|
|
|
|
HelpCircle,
|
|
|
|
|
Plus,
|
|
|
|
|
RefreshCw,
|
|
|
|
|
Sparkles,
|
|
|
|
|
} from 'lucide-react';
|
2026-05-05 03:05:22 +02:00
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
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>
2026-05-21 17:28:20 +02:00
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
2026-05-05 03:05:22 +02:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select';
|
|
|
|
|
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
|
|
|
|
|
import { AddBerthToInterestDialog } from '@/components/interests/add-berth-to-interest-dialog';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
// ─── Types (mirror the recommender service Recommendation shape) ───────────
|
|
|
|
|
|
|
|
|
|
type Tier = 'A' | 'B' | 'C' | 'D';
|
|
|
|
|
|
|
|
|
|
interface HeatBreakdown {
|
|
|
|
|
recency: number;
|
|
|
|
|
furthestStage: number;
|
|
|
|
|
interestCount: number;
|
|
|
|
|
eoiCount: number;
|
|
|
|
|
total: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Recommendation {
|
|
|
|
|
berthId: string;
|
|
|
|
|
mooringNumber: string;
|
|
|
|
|
area: string | null;
|
|
|
|
|
tier: Tier;
|
|
|
|
|
fitScore: number;
|
|
|
|
|
sizeBufferPct: number | null;
|
|
|
|
|
heat: HeatBreakdown | null;
|
|
|
|
|
reasons: {
|
|
|
|
|
dimensional: string;
|
|
|
|
|
pipeline: string;
|
|
|
|
|
amenities?: string;
|
|
|
|
|
heat?: string;
|
|
|
|
|
};
|
|
|
|
|
lengthFt: number | null;
|
|
|
|
|
widthFt: number | null;
|
|
|
|
|
draftFt: number | null;
|
|
|
|
|
status: string;
|
|
|
|
|
amenities: {
|
|
|
|
|
powerCapacity: number | null;
|
|
|
|
|
voltage: number | null;
|
|
|
|
|
access: string | null;
|
|
|
|
|
mooringType: string | null;
|
|
|
|
|
cleatCapacity: string | null;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AmenityFilters {
|
|
|
|
|
minPowerCapacityKw?: number;
|
|
|
|
|
requiredVoltage?: number;
|
|
|
|
|
requiredAccess?: string;
|
|
|
|
|
requiredMooringType?: string;
|
|
|
|
|
requiredCleatCapacity?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface BerthRecommenderPanelProps {
|
|
|
|
|
interestId: string;
|
|
|
|
|
/** Display label for the dimensions in the header. */
|
|
|
|
|
desiredLengthFt: number | null;
|
|
|
|
|
desiredWidthFt: number | null;
|
|
|
|
|
desiredDraftFt: number | null;
|
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
|
|
|
/**
|
|
|
|
|
* Unit the rep originally entered the dimensions in. Drives header
|
|
|
|
|
* display so a metric-entered deal doesn't render its dims as ft.
|
|
|
|
|
* Falls back to 'ft' when missing.
|
|
|
|
|
*/
|
|
|
|
|
desiredUnit?: 'ft' | 'm' | null;
|
2026-05-20 15:56:11 +02:00
|
|
|
/**
|
|
|
|
|
* Number of berths already linked to the interest. When ≥ 1 the panel
|
|
|
|
|
* defaults to collapsed (header-only) so the LinkedBerthsList card above
|
|
|
|
|
* dominates the rep's attention. They can expand to browse more options
|
|
|
|
|
* (multi-berth deals, swap recommendations). Zero / undefined keeps the
|
|
|
|
|
* panel expanded so reps see options immediately.
|
|
|
|
|
*/
|
|
|
|
|
linkedBerthCount?: number;
|
2026-05-05 03:05:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TIER_LABELS: Record<Tier, { label: string; tone: string }> = {
|
|
|
|
|
A: { label: 'Open', tone: 'border-emerald-200 bg-emerald-50 text-emerald-800' },
|
|
|
|
|
B: { label: 'Fall-through', tone: 'border-amber-200 bg-amber-50 text-amber-800' },
|
|
|
|
|
C: { label: 'Active interest', tone: 'border-sky-200 bg-sky-50 text-sky-800' },
|
|
|
|
|
D: { label: 'Late stage', tone: 'border-slate-300 bg-slate-100 text-slate-700' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function statusToPill(status: string): StatusPillStatus {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'available':
|
|
|
|
|
return 'active';
|
|
|
|
|
case 'under_offer':
|
|
|
|
|
return 'sent';
|
|
|
|
|
case 'sold':
|
|
|
|
|
return 'completed';
|
|
|
|
|
case 'reserved':
|
|
|
|
|
return 'partial';
|
|
|
|
|
default:
|
|
|
|
|
return 'pending';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatStatus(status: string): string {
|
|
|
|
|
return status.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDimensions(
|
|
|
|
|
length: number | null,
|
|
|
|
|
width: number | null,
|
|
|
|
|
draft: number | null,
|
|
|
|
|
): string {
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
if (length !== null) parts.push(`${length.toFixed(1)}ft L`);
|
|
|
|
|
if (width !== null) parts.push(`${width.toFixed(1)}ft W`);
|
|
|
|
|
if (draft !== null) parts.push(`${draft.toFixed(1)}ft D`);
|
|
|
|
|
return parts.join(' · ');
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
function formatDesired(
|
|
|
|
|
length: number | null,
|
|
|
|
|
width: number | null,
|
|
|
|
|
draft: number | null,
|
|
|
|
|
unit: 'ft' | 'm' = 'ft',
|
|
|
|
|
): string {
|
|
|
|
|
// Storage is canonical-ft (the recommender's SQL ranks against
|
|
|
|
|
// berths.length_ft etc.). For display we convert back to whatever the rep
|
|
|
|
|
// entered. 0.3048 m/ft exactly.
|
|
|
|
|
const toDisplay = (ft: number): string => {
|
|
|
|
|
const v = unit === 'm' ? ft * 0.3048 : ft;
|
|
|
|
|
return v.toFixed(2).replace(/\.?0+$/, '');
|
|
|
|
|
};
|
2026-05-05 03:05:22 +02:00
|
|
|
const parts: string[] = [];
|
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
|
|
|
if (length !== null) parts.push(`${toDisplay(length)}${unit} L`);
|
|
|
|
|
if (width !== null) parts.push(`${toDisplay(width)}${unit} W`);
|
|
|
|
|
if (draft !== null) parts.push(`${toDisplay(draft)}${unit} D`);
|
2026-05-05 03:05:22 +02:00
|
|
|
return parts.length > 0 ? parts.join(' · ') : 'no dimensions set';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RecommendationCardProps {
|
|
|
|
|
rec: Recommendation;
|
|
|
|
|
portSlug: string;
|
|
|
|
|
onAdd: (rec: Recommendation) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RecommendationCard({ rec, portSlug, onAdd }: RecommendationCardProps) {
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
|
const tier = TIER_LABELS[rec.tier];
|
|
|
|
|
const showHeat = rec.heat && rec.heat.total > 0;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="rounded-lg border bg-card text-sm">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setExpanded((v) => !v)}
|
|
|
|
|
className="flex w-full items-start gap-3 p-3 text-left hover:bg-muted/40"
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0 flex-1 space-y-1">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
<span className="font-semibold">{rec.mooringNumber}</span>
|
|
|
|
|
{rec.area ? <span className="text-xs text-muted-foreground">{rec.area}</span> : null}
|
|
|
|
|
<StatusPill status={statusToPill(rec.status)}>{formatStatus(rec.status)}</StatusPill>
|
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>
2026-05-21 17:28:20 +02:00
|
|
|
<Popover>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className={cn(
|
|
|
|
|
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
|
|
|
tier.tone,
|
|
|
|
|
)}
|
|
|
|
|
aria-label={`Recommender state: ${tier.label}`}
|
|
|
|
|
>
|
|
|
|
|
{tier.label}
|
|
|
|
|
<HelpCircle className="size-3 opacity-60" aria-hidden />
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent align="start" className="w-80 text-xs leading-relaxed">
|
|
|
|
|
<p className="font-medium text-foreground">Recommender state</p>
|
|
|
|
|
<ul className="mt-2 space-y-1.5 text-muted-foreground">
|
|
|
|
|
<li>
|
|
|
|
|
<span className="font-medium text-emerald-700">Open</span>: never had an
|
|
|
|
|
interest, ready for new prospects.
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
<span className="font-medium text-amber-700">Fall-through</span>: a prior
|
|
|
|
|
interest didn't close; warm and worth pitching again.
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
<span className="font-medium text-sky-700">Active interest</span>: another deal
|
|
|
|
|
is in play. Coordinate before pitching.
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
<span className="font-medium text-slate-700">Late stage</span>: another deal is
|
|
|
|
|
near-sold; treat as backup only.
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-05-05 03:05:22 +02:00
|
|
|
{showHeat ? (
|
|
|
|
|
<span className="inline-flex items-center gap-1 rounded-md border border-rose-200 bg-rose-50 px-2 py-0.5 text-xs font-medium text-rose-800">
|
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
|
|
|
<Flame className="size-3" aria-hidden />
|
2026-05-05 03:05:22 +02:00
|
|
|
Heat {Math.round(rec.heat!.total)}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
{formatDimensions(rec.lengthFt, rec.widthFt, rec.draftFt)}
|
|
|
|
|
{rec.sizeBufferPct !== null ? (
|
|
|
|
|
<span>
|
|
|
|
|
{' '}
|
|
|
|
|
· {rec.sizeBufferPct >= 0 ? '+' : ''}
|
|
|
|
|
{rec.sizeBufferPct}% vs desired
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
<span className="ml-2 font-medium text-foreground">Fit {rec.fitScore}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{expanded ? (
|
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
|
|
|
<ChevronUp className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
2026-05-05 03:05:22 +02:00
|
|
|
) : (
|
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
|
|
|
<ChevronDown className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
2026-05-05 03:05:22 +02:00
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{expanded ? (
|
|
|
|
|
<div className="space-y-3 border-t bg-muted/20 p-3">
|
|
|
|
|
<dl className="space-y-1 text-xs">
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<dt className="w-28 shrink-0 text-muted-foreground">Dimensional</dt>
|
|
|
|
|
<dd>{rec.reasons.dimensional}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<dt className="w-28 shrink-0 text-muted-foreground">Pipeline</dt>
|
|
|
|
|
<dd>{rec.reasons.pipeline}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
{rec.reasons.amenities ? (
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<dt className="w-28 shrink-0 text-muted-foreground">Amenities</dt>
|
|
|
|
|
<dd>{rec.reasons.amenities}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
{rec.reasons.heat ? (
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<dt className="w-28 shrink-0 text-muted-foreground">Heat</dt>
|
|
|
|
|
<dd>{rec.reasons.heat}</dd>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</dl>
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onAdd(rec);
|
|
|
|
|
}}
|
|
|
|
|
>
|
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
|
|
|
<Plus className="mr-1.5 size-3.5" aria-hidden />
|
2026-05-05 03:05:22 +02:00
|
|
|
Add to interest
|
|
|
|
|
</Button>
|
|
|
|
|
<Button asChild size="sm" variant="outline">
|
|
|
|
|
<Link href={`/${portSlug}/berths/${rec.berthId}`}>View berth</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface AmenityFilterFormProps {
|
|
|
|
|
filters: AmenityFilters;
|
|
|
|
|
onChange: (next: AmenityFilters) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AmenityFilterForm({ filters, onChange }: AmenityFilterFormProps) {
|
|
|
|
|
const update = <K extends keyof AmenityFilters>(key: K, value: AmenityFilters[K]) => {
|
|
|
|
|
const next = { ...filters };
|
|
|
|
|
if (value === undefined || value === '' || (typeof value === 'number' && Number.isNaN(value))) {
|
|
|
|
|
delete next[key];
|
|
|
|
|
} else {
|
|
|
|
|
next[key] = value;
|
|
|
|
|
}
|
|
|
|
|
onChange(next);
|
|
|
|
|
};
|
|
|
|
|
return (
|
|
|
|
|
<div className="grid grid-cols-1 gap-3 rounded-lg border bg-muted/20 p-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="filter-power" className="text-xs">
|
|
|
|
|
Min power (kW)
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="filter-power"
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
step="0.1"
|
|
|
|
|
value={filters.minPowerCapacityKw ?? ''}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
update('minPowerCapacityKw', e.target.value ? parseFloat(e.target.value) : undefined)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="filter-voltage" className="text-xs">
|
|
|
|
|
Voltage
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="filter-voltage"
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
step="1"
|
|
|
|
|
value={filters.requiredVoltage ?? ''}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
update('requiredVoltage', e.target.value ? parseInt(e.target.value, 10) : undefined)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="filter-mooring" className="text-xs">
|
|
|
|
|
Mooring type
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={filters.requiredMooringType ?? ''}
|
|
|
|
|
onValueChange={(v) => update('requiredMooringType', v || undefined)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="filter-mooring">
|
|
|
|
|
<SelectValue placeholder="Any" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="stern_to">Stern-to</SelectItem>
|
|
|
|
|
<SelectItem value="alongside">Alongside</SelectItem>
|
|
|
|
|
<SelectItem value="bow_to">Bow-to</SelectItem>
|
|
|
|
|
<SelectItem value="swing">Swing mooring</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="filter-cleat" className="text-xs">
|
|
|
|
|
Cleat capacity
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={filters.requiredCleatCapacity ?? ''}
|
|
|
|
|
onValueChange={(v) => update('requiredCleatCapacity', v || undefined)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="filter-cleat">
|
|
|
|
|
<SelectValue placeholder="Any" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="standard">Standard</SelectItem>
|
|
|
|
|
<SelectItem value="heavy">Heavy</SelectItem>
|
|
|
|
|
<SelectItem value="superyacht">Superyacht</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="filter-access" className="text-xs">
|
|
|
|
|
Access
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={filters.requiredAccess ?? ''}
|
|
|
|
|
onValueChange={(v) => update('requiredAccess', v || undefined)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="filter-access">
|
|
|
|
|
<SelectValue placeholder="Any" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="land">Land access</SelectItem>
|
|
|
|
|
<SelectItem value="dinghy">Dinghy only</SelectItem>
|
|
|
|
|
<SelectItem value="walk_on">Walk-on</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// destructure includes `desiredUnit` so the header formatter pivots on the
|
|
|
|
|
// rep's entered unit. Falls back to 'ft' (the legacy default) when missing.
|
2026-05-05 03:05:22 +02:00
|
|
|
export function BerthRecommenderPanel({
|
|
|
|
|
interestId,
|
|
|
|
|
desiredLengthFt,
|
|
|
|
|
desiredWidthFt,
|
|
|
|
|
desiredDraftFt,
|
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
|
|
|
desiredUnit,
|
2026-05-20 15:56:11 +02:00
|
|
|
linkedBerthCount,
|
2026-05-05 03:05:22 +02:00
|
|
|
}: BerthRecommenderPanelProps) {
|
|
|
|
|
const params = useParams<{ portSlug: string }>();
|
|
|
|
|
const portSlug = params?.portSlug ?? '';
|
|
|
|
|
|
|
|
|
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
|
|
|
const [amenityFilters, setAmenityFilters] = useState<AmenityFilters>({});
|
|
|
|
|
const [showAll, setShowAll] = useState(false);
|
|
|
|
|
const [pendingBerth, setPendingBerth] = useState<Recommendation | null>(null);
|
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
|
|
|
// Area-letter filter — chips above the list let reps narrow to a
|
|
|
|
|
// single pier (e.g. "show me only A-row matches"). Client-side over
|
|
|
|
|
// the already-fetched result set; no service change required.
|
|
|
|
|
const [selectedAreas, setSelectedAreas] = useState<string[]>([]);
|
2026-05-20 15:56:11 +02:00
|
|
|
// Collapse state — defaults to collapsed when the deal already has at
|
|
|
|
|
// least one linked berth (recommender becomes a "browse more options"
|
|
|
|
|
// tool rather than the primary surface). Reps can manually expand any
|
|
|
|
|
// time. Header click toggles.
|
|
|
|
|
const [collapsed, setCollapsed] = useState<boolean>((linkedBerthCount ?? 0) > 0);
|
2026-05-05 03:05:22 +02:00
|
|
|
|
|
|
|
|
const hasDimensions = desiredLengthFt !== null;
|
|
|
|
|
|
|
|
|
|
const queryKey = useMemo(
|
|
|
|
|
() => ['berth-recommendations', interestId, amenityFilters, showAll] as const,
|
|
|
|
|
[interestId, amenityFilters, showAll],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { data, isFetching, refetch } = useQuery({
|
|
|
|
|
queryKey,
|
2026-05-20 15:56:11 +02:00
|
|
|
// Skip the network call when collapsed — no point fetching options
|
|
|
|
|
// the rep won't see. Re-fires automatically on expand.
|
|
|
|
|
enabled: hasDimensions && !collapsed,
|
2026-05-05 03:05:22 +02:00
|
|
|
queryFn: () =>
|
|
|
|
|
apiFetch<{ data: Recommendation[] }>(`/api/v1/interests/${interestId}/recommend-berths`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: {
|
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
|
|
|
// `showAll` opens the floodgates: bumps `topN` AND raises the
|
|
|
|
|
// oversize-cap so berths well beyond the strict feasibility window
|
|
|
|
|
// surface. Without that second bump the user could end up staring
|
|
|
|
|
// at "no berths match" when the test data only had oversized rows
|
|
|
|
|
// — exactly the case in our seeded demo port.
|
|
|
|
|
...(showAll ? { topN: 999, maxOversizePct: 1000 } : {}),
|
2026-05-05 03:05:22 +02:00
|
|
|
...(Object.keys(amenityFilters).length > 0 ? { amenityFilters } : {}),
|
|
|
|
|
},
|
|
|
|
|
}).then((r) => r.data),
|
|
|
|
|
staleTime: 60_000,
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
const allRecommendations = data ?? [];
|
|
|
|
|
|
|
|
|
|
// Build the set of dock-letter chips from whatever came back, then
|
|
|
|
|
// filter the visible recommendations by the active selection. Empty
|
|
|
|
|
// selection = show everything (default).
|
|
|
|
|
const areaChips = useMemo(() => {
|
|
|
|
|
const set = new Set<string>();
|
|
|
|
|
for (const r of allRecommendations) {
|
|
|
|
|
const m = r.mooringNumber.match(/^([A-Z]+)/);
|
|
|
|
|
if (m?.[1]) set.add(m[1]);
|
|
|
|
|
}
|
|
|
|
|
return Array.from(set).sort();
|
|
|
|
|
}, [allRecommendations]);
|
|
|
|
|
|
|
|
|
|
const recommendations =
|
|
|
|
|
selectedAreas.length === 0
|
|
|
|
|
? allRecommendations
|
|
|
|
|
: allRecommendations.filter((r) => {
|
|
|
|
|
const m = r.mooringNumber.match(/^([A-Z]+)/);
|
|
|
|
|
return m?.[1] ? selectedAreas.includes(m[1]) : false;
|
|
|
|
|
});
|
2026-05-05 03:05:22 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="gap-3">
|
|
|
|
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
|
|
|
<div className="min-w-0 space-y-1">
|
|
|
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
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
|
|
|
<Sparkles className="size-4 text-brand-600" aria-hidden />
|
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
|
|
|
Recommendations for{' '}
|
|
|
|
|
{formatDesired(
|
|
|
|
|
desiredLengthFt,
|
|
|
|
|
desiredWidthFt,
|
|
|
|
|
desiredDraftFt,
|
|
|
|
|
desiredUnit === 'm' ? 'm' : 'ft',
|
|
|
|
|
)}
|
2026-05-05 03:05:22 +02:00
|
|
|
</CardTitle>
|
|
|
|
|
{!hasDimensions ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Set desired dimensions to see recommendations.
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
2026-05-20 15:56:11 +02:00
|
|
|
{!collapsed ? (
|
|
|
|
|
<>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setFiltersOpen((v) => !v)}
|
|
|
|
|
disabled={!hasDimensions}
|
|
|
|
|
>
|
|
|
|
|
<Filter className="mr-1.5 size-3.5" aria-hidden />
|
|
|
|
|
{filtersOpen ? 'Hide filters' : 'Add filters'}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => refetch()}
|
|
|
|
|
disabled={!hasDimensions || isFetching}
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw className={cn('mr-1.5 size-3.5', isFetching && 'animate-spin')} />
|
|
|
|
|
Refresh
|
|
|
|
|
</Button>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
2026-05-05 03:05:22 +02:00
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
2026-05-20 15:56:11 +02:00
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setCollapsed((v) => !v)}
|
|
|
|
|
aria-expanded={!collapsed}
|
|
|
|
|
aria-controls={`recommender-body-${interestId}`}
|
2026-05-05 03:05:22 +02:00
|
|
|
>
|
2026-05-20 15:56:11 +02:00
|
|
|
{collapsed ? (
|
|
|
|
|
<>
|
|
|
|
|
<ChevronDown className="mr-1.5 size-3.5" aria-hidden />
|
|
|
|
|
Show recommendations
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<ChevronUp className="mr-1.5 size-3.5" aria-hidden />
|
|
|
|
|
Hide
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-05-05 03:05:22 +02:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-20 15:56:11 +02:00
|
|
|
{!collapsed && filtersOpen && hasDimensions ? (
|
2026-05-05 03:05:22 +02:00
|
|
|
<AmenityFilterForm filters={amenityFilters} onChange={setAmenityFilters} />
|
|
|
|
|
) : null}
|
2026-05-20 15:56:11 +02:00
|
|
|
{!collapsed && hasDimensions && areaChips.length > 1 ? (
|
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
|
|
|
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Area:</span>
|
|
|
|
|
{areaChips.map((letter) => {
|
|
|
|
|
const active = selectedAreas.includes(letter);
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={letter}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
setSelectedAreas((prev) =>
|
|
|
|
|
prev.includes(letter) ? prev.filter((l) => l !== letter) : [...prev, letter],
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
className={cn(
|
|
|
|
|
'rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors',
|
|
|
|
|
active
|
|
|
|
|
? 'border-primary bg-primary text-primary-foreground'
|
|
|
|
|
: 'border-input bg-background text-foreground hover:bg-muted',
|
|
|
|
|
)}
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
>
|
|
|
|
|
{letter}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{selectedAreas.length > 0 ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setSelectedAreas([])}
|
|
|
|
|
className="text-xs text-muted-foreground underline ml-1"
|
|
|
|
|
>
|
|
|
|
|
Clear
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-05-05 03:05:22 +02:00
|
|
|
</CardHeader>
|
2026-05-20 15:56:11 +02:00
|
|
|
{collapsed ? null : (
|
|
|
|
|
<CardContent className="space-y-3" id={`recommender-body-${interestId}`}>
|
|
|
|
|
{!hasDimensions ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Once length, width, and draft are set on this interest, the recommender will surface
|
|
|
|
|
berths that fit. Edit the desired dimensions on the{' '}
|
|
|
|
|
<Link href="?tab=overview" className="text-primary underline">
|
|
|
|
|
Overview tab
|
|
|
|
|
</Link>
|
|
|
|
|
.
|
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
|
|
|
</p>
|
2026-05-20 15:56:11 +02:00
|
|
|
) : isFetching && recommendations.length === 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{[0, 1, 2].map((i) => (
|
|
|
|
|
<div key={i} className="h-16 animate-pulse rounded-lg bg-muted" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : recommendations.length === 0 ? (
|
|
|
|
|
<div className="py-6 text-center text-sm text-muted-foreground space-y-2">
|
|
|
|
|
<p>
|
|
|
|
|
{showAll
|
|
|
|
|
? 'No berths in the port match these dimensions and filters.'
|
|
|
|
|
: 'No berths fit inside the strict oversize tolerance.'}
|
|
|
|
|
</p>
|
|
|
|
|
{!showAll && (
|
|
|
|
|
<Button type="button" size="sm" variant="outline" onClick={() => setShowAll(true)}>
|
|
|
|
|
Show oversized matches too
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{recommendations.map((rec) => (
|
|
|
|
|
<RecommendationCard
|
|
|
|
|
key={rec.berthId}
|
|
|
|
|
rec={rec}
|
|
|
|
|
portSlug={portSlug}
|
|
|
|
|
onAdd={setPendingBerth}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{hasDimensions && recommendations.length > 0 ? (
|
|
|
|
|
<div className="flex justify-center pt-1">
|
|
|
|
|
<Button type="button" size="sm" variant="ghost" onClick={() => setShowAll((v) => !v)}>
|
|
|
|
|
{showAll ? 'Show top in-tolerance only' : 'Show oversized matches too'}
|
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
|
|
|
</Button>
|
2026-05-20 15:56:11 +02:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</CardContent>
|
|
|
|
|
)}
|
2026-05-05 03:05:22 +02:00
|
|
|
|
|
|
|
|
{pendingBerth ? (
|
|
|
|
|
<AddBerthToInterestDialog
|
|
|
|
|
interestId={interestId}
|
|
|
|
|
berth={{
|
|
|
|
|
berthId: pendingBerth.berthId,
|
|
|
|
|
mooringNumber: pendingBerth.mooringNumber,
|
|
|
|
|
}}
|
|
|
|
|
open={pendingBerth !== null}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) setPendingBerth(null);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|