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>
This commit is contained in:
2026-05-21 17:41:02 +02:00
parent a673b6cec2
commit c6dcf49e18
8 changed files with 89 additions and 23 deletions

View File

@@ -12,6 +12,8 @@ import { interests, interestBerths } from '@/lib/db/schema/interests';
import { clients } from '@/lib/db/schema/clients';
import { companies } from '@/lib/db/schema/companies';
import { yachts } from '@/lib/db/schema/yachts';
import { berths } from '@/lib/db/schema/berths';
import { formatBerthRange } from '@/lib/templates/berth-range';
import { berthReservations } from '@/lib/db/schema/reservations';
import { ports } from '@/lib/db/schema/ports';
import { userProfiles, userPortRoles } from '@/lib/db/schema/users';
@@ -2019,7 +2021,7 @@ export interface DocumentDetailWatcher {
* Each side is null when the FK is null or the row was deleted.
*/
export interface DocumentDetailLinkedEntities {
interest: { id: string; clientName: string | null } | null;
interest: { id: string; clientName: string | null; berthLabel: string | null } | null;
client: { id: string; fullName: string } | null;
yacht: { id: string; name: string } | null;
company: { id: string; name: string } | null;
@@ -2097,9 +2099,40 @@ export async function getDocumentDetail(id: string, portId: string): Promise<Doc
: Promise.resolve(undefined),
]);
// Derive the berth label so the doc-detail Interest link carries
// distinct information from the Client link (otherwise both render
// the same client name). Prefer the in-EOI-bundle subset; fall back
// to the primary; fall back to all linked berths if neither flag is
// set anywhere.
let interestBerthLabel: string | null = null;
if (interestRow) {
const berthRows = await db
.select({
mooringNumber: berths.mooringNumber,
isPrimary: interestBerths.isPrimary,
isInEoiBundle: interestBerths.isInEoiBundle,
})
.from(interestBerths)
.innerJoin(berths, eq(berths.id, interestBerths.berthId))
.where(eq(interestBerths.interestId, interestRow.id));
if (berthRows.length > 0) {
const bundled = berthRows.filter((r) => r.isInEoiBundle);
const primary = berthRows.filter((r) => r.isPrimary);
const subset = bundled.length > 0 ? bundled : primary.length > 0 ? primary : berthRows;
const moorings = subset.map((r) => r.mooringNumber).filter((m): m is string => !!m);
if (moorings.length > 0) {
interestBerthLabel = formatBerthRange(moorings);
}
}
}
const linked: DocumentDetailLinkedEntities = {
interest: interestRow
? { id: interestRow.id, clientName: interestRow.clientName ?? null }
? {
id: interestRow.id,
clientName: interestRow.clientName ?? null,
berthLabel: interestBerthLabel,
}
: null,
client: clientRow ? { id: clientRow.id, fullName: clientRow.fullName } : null,
yacht: yachtRow ? { id: yachtRow.id, name: yachtRow.name } : null,