feat(uat-polish): live-UAT round — dialog widths, recommender polish, inline create, tenancy + notes plumbing
Compendium of polish + small-fix work captured during the 2026-05-26
live UAT session. Every change has a corresponding entry in
docs/superpowers/audits/active-uat.md with file:line evidence + root
cause + alternatives considered.
Dialog primitive width
- DialogContent default bumped from sm:max-w-lg (512px) to
sm:max-w-xl + lg:max-w-3xl so every consumer gets a sane desktop
default. Confirm dialogs override DOWN, content-heavy dialogs
override UP.
- FilePreviewDialog full-viewport via w-[min(95vw,1400px)] +
h-[85vh] so PDFs render at usable width on real desktops.
Recommender card
- Heat badge now a Popover with the score (X/100), the formula in
plain English, the four component breakdowns (recency / furthest
stage / interest count / EOI count), and a pointer to the admin
weight tuning page.
- Area letter span dropped from the card header - mooring number
already prefixes it.
- BerthRecommenderPanel + the dedicated "Berth Recommendations" tab
both hidden when interest.desiredLengthFt is null. The empty
guidance card was reading as noise. interest-tabs.tsx computes
hasDesiredDims once and gates the inline mount + tab strip
spread off it.
BerthPicker
- Drop area suffix from row labels. Mooring number already carries
the area letter prefix; group heading conveys the same context.
Same fix flows to every BerthPicker consumer (tenancy
create/renew/transfer, interest form, linked-berths picker).
CreateDocumentWizard
- DOCUMENT_TYPE_LABELS constant added to constants.ts. Wizard reads
from the map instead of naive replace(/_/g, ' '): "EOI",
"Contract", "NDA", "Reservation Agreement", "Other".
- "Other" option surfaces a hint pointing the rep at the Title
field so they describe what the doc actually is.
InterestForm inline client + yacht create
- ClientForm gains an onCreated(clientId) callback. Mutation
returns { id } in create mode so onSuccess can forward.
- InterestForm renders an "Add new" Button next to the Client label
(create mode only - hidden on edit), opens ClientForm, auto-
selects the new client into the draft. Mirrors the existing
inline yacht-create pattern.
- Reset path includes source: 'manual' alongside the other create-
mode defaults; the manual flow was dropping back to a blank
source dropdown on reopen.
Tenancy list
- ClientTenanciesTab activeTenancies query now includes status
IN ('pending', 'active'). Was filtering to active-only; pending
rows from manual create + webhook auto-create were invisible on
the client detail's Tenancies tab.
- TenancyList rows are now keyboard- and click-navigable to the
tenancy detail page (Enter/Space included). Inner links + buttons
stop propagation so per-cell navigation works.
NotesList source badge
- Aggregated-mode source badge ("Yacht / Test Yacht") is now a Link
to the source entity's detail page. New sourceLinkFor helper
centralises the URL mapping across clients/companies/yachts/
interests + residential variants.
Yacht transfer audit log
- transferOwnership emits a distinct 'transfer' AuditAction (added
to AuditAction union in src/lib/audit.ts) with old/new owner
names resolved at write time. EntityActivityFeed renders
"Matt transferred owner to Jane Smith" instead of "Matt updated
this record." formatValueForField unwraps the { name } shape so
the audit_logs Record<string, unknown> typing stays clean.
- yacht-transfer-dialog copy: dropped "atomic" jargon. Reads "The
change is logged in the audit history" instead.
Companies autocomplete
- /api/v1/companies/autocomplete now returns the 10 most-recently-
updated companies when the query string is empty. Was returning
[]. CompanyPicker popover opens with results to scan instead of a
blank dropdown.
DocumentsHub FlatFolderListing
- Uploaded files (the files table) now merge into the documents
table view via a parallel /api/v1/files?folderId=X query +
client-side merge into a unified row list. listFiles service
honours the folderId filter that was already accepted by the
validator. New renderFileRow renders file rows with an "Uploaded
file" type pill + "Stored" status pill, links the filename to
the download URL. Existing FolderDropZone invalidation covers
the new query, so drag-drop and New-document-menu uploads
refresh the list without a page reload.
- FlatFolderListing wrapped in a vertically-spaced container so
subfolders / search row / list have consistent gap.
- Per-row chevron only renders when totalSigners > 0; empty
placeholder column kept so grid alignment doesn't jump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useQuery, useMutation, useQueryClient, type QueryKey } from '@tanstack/react-query';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { Lock, Pencil, Trash2, Send, Loader2 } from 'lucide-react';
|
||||
@@ -134,6 +136,28 @@ function sortByGroup(notes: Note[]): Note[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve the detail-page URL for a note's source entity so the
|
||||
* aggregated-mode source badge can navigate the rep to that record. */
|
||||
function sourceLinkFor(portSlug: string, source: NoteSource, sourceId: string): string | null {
|
||||
if (!portSlug) return null;
|
||||
switch (source) {
|
||||
case 'client':
|
||||
return `/${portSlug}/clients/${sourceId}`;
|
||||
case 'company':
|
||||
return `/${portSlug}/companies/${sourceId}`;
|
||||
case 'yacht':
|
||||
return `/${portSlug}/yachts/${sourceId}`;
|
||||
case 'interest':
|
||||
return `/${portSlug}/interests/${sourceId}`;
|
||||
case 'residential_client':
|
||||
return `/${portSlug}/residential/clients/${sourceId}`;
|
||||
case 'residential_interest':
|
||||
return `/${portSlug}/residential/interests/${sourceId}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function NotesList({
|
||||
entityType,
|
||||
entityId,
|
||||
@@ -142,6 +166,8 @@ export function NotesList({
|
||||
parentInvalidateKey,
|
||||
}: NotesListProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const routeParams = useParams<{ portSlug: string }>();
|
||||
const portSlug = routeParams?.portSlug ?? '';
|
||||
const invalidateAll = () => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
if (parentInvalidateKey) {
|
||||
@@ -296,14 +322,34 @@ export function NotesList({
|
||||
{aggregateOn &&
|
||||
note.source &&
|
||||
note.source !== SELF_SOURCE[entityType] &&
|
||||
note.sourceLabel && (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${SOURCE_BADGE_CLASS[note.source]}`}
|
||||
title={`From ${note.source}`}
|
||||
>
|
||||
{SOURCE_LABEL[note.source]} · {note.sourceLabel}
|
||||
</span>
|
||||
)}
|
||||
note.sourceLabel &&
|
||||
(() => {
|
||||
// Source badge links to the originating entity so reps
|
||||
// can pivot from "this note about a linked yacht" to
|
||||
// the yacht detail page directly. Falls back to a
|
||||
// plain span when no sourceId is present (rare; aggregator
|
||||
// returns it for every materialised note).
|
||||
const sourceHref = note.sourceId
|
||||
? sourceLinkFor(portSlug, note.source, note.sourceId)
|
||||
: null;
|
||||
const className = `inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium ${SOURCE_BADGE_CLASS[note.source]} ${sourceHref ? 'hover:opacity-80 transition-opacity' : ''}`;
|
||||
const body = `${SOURCE_LABEL[note.source]} · ${note.sourceLabel}`;
|
||||
const title = `Open this ${note.source}`;
|
||||
return sourceHref ? (
|
||||
<Link
|
||||
href={sourceHref as never}
|
||||
className={className}
|
||||
title={title}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{body}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={className} title={`From ${note.source}`}>
|
||||
{body}
|
||||
</span>
|
||||
);
|
||||
})()}
|
||||
{/* Pipeline-stage stamp: shows what stage the linked
|
||||
interest was at when the note was authored. Lets a
|
||||
rep trace how the deal's notes evolved (concerns
|
||||
|
||||
Reference in New Issue
Block a user