diff --git a/src/app/(dashboard)/[portSlug]/expenses/page.tsx b/src/app/(dashboard)/[portSlug]/expenses/page.tsx
index 40088159..7c5c754d 100644
--- a/src/app/(dashboard)/[portSlug]/expenses/page.tsx
+++ b/src/app/(dashboard)/[portSlug]/expenses/page.tsx
@@ -105,7 +105,7 @@ export default function ExpensesPage() {
diff --git a/src/app/(dashboard)/[portSlug]/residential/page.tsx b/src/app/(dashboard)/[portSlug]/residential/page.tsx
new file mode 100644
index 00000000..e3ea7480
--- /dev/null
+++ b/src/app/(dashboard)/[portSlug]/residential/page.tsx
@@ -0,0 +1,16 @@
+import { redirect } from 'next/navigation';
+
+/**
+ * //residential is a namespace segment — the actual landing is
+ * /residential/clients. Without a page.tsx here, the breadcrumb's
+ * "Residential" link 404s. Server-redirect to the Clients sub-page so
+ * the link works as a useful shortcut.
+ */
+export default async function ResidentialIndexPage({
+ params,
+}: {
+ params: Promise<{ portSlug: string }>;
+}) {
+ const { portSlug } = await params;
+ redirect(`/${portSlug}/residential/clients`);
+}
diff --git a/src/components/documents/document-detail.tsx b/src/components/documents/document-detail.tsx
index de7440b0..265a272b 100644
--- a/src/components/documents/document-detail.tsx
+++ b/src/components/documents/document-detail.tsx
@@ -93,7 +93,7 @@ interface DetailWatcher {
}
interface DetailLinked {
- 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;
@@ -238,7 +238,11 @@ export function DocumentDetail({ documentId, portSlug }: DocumentDetailProps) {
linkedRows.push({
href: `/${portSlug}/interests/${linked.interest.id}`,
label: 'Interest',
- sub: linked.interest.clientName,
+ // Show the berth label (e.g. "A1-A3, B5-B7" or "A12") so the
+ // Interest link carries distinct information from the Client
+ // link rendered just below — otherwise both rows show the same
+ // client name and the Interest row reads as duplicate.
+ sub: linked.interest.berthLabel ?? linked.interest.clientName ?? 'No berths linked',
});
}
if (linked.client) {
diff --git a/src/components/interests/interest-eoi-tab.tsx b/src/components/interests/interest-eoi-tab.tsx
index a7c63a35..690256bc 100644
--- a/src/components/interests/interest-eoi-tab.tsx
+++ b/src/components/interests/interest-eoi-tab.tsx
@@ -177,7 +177,7 @@ export function InterestEoiTab({ interestId, clientId }: InterestEoiTabProps) {
href={`/${portSlug}/documents/${d.id}` as any}
className="text-xs text-primary hover:underline inline-flex items-center gap-1"
>
- Open
+ Open in Documents
)}
diff --git a/src/components/residential/residential-client-tabs.tsx b/src/components/residential/residential-client-tabs.tsx
index a5f271ca..930a5c3e 100644
--- a/src/components/residential/residential-client-tabs.tsx
+++ b/src/components/residential/residential-client-tabs.tsx
@@ -273,17 +273,16 @@ function InterestsTab({
) : (
{client.interests.map((i) => (
-
-
- {stageLabels[i.pipelineStage] ?? i.pipelineStage}
-
- {i.preferences || i.notes || '-'}
+
- View
+
+ {stageLabels[i.pipelineStage] ?? i.pipelineStage}
+
+ {i.preferences || i.notes || '-'}
))}
diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx
index 96b48cf9..469a16cf 100644
--- a/src/components/ui/dropdown-menu.tsx
+++ b/src/components/ui/dropdown-menu.tsx
@@ -63,7 +63,10 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- 'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
+ // Cap at 24rem (384px) so long menus don't visually stretch
+ // edge-to-edge of the viewport — internal scroll handles
+ // overflow. Consumers can override via the `className` prop.
+ 'z-50 max-h-96 min-w-32 overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)',
className,
)}
diff --git a/src/components/yachts/yacht-tabs.tsx b/src/components/yachts/yacht-tabs.tsx
index b617856a..3f79e209 100644
--- a/src/components/yachts/yacht-tabs.tsx
+++ b/src/components/yachts/yacht-tabs.tsx
@@ -88,7 +88,15 @@ function EditableRow({ label, children }: { label: string; children: React.React
);
}
-function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYacht }) {
+function OverviewTab({
+ yachtId,
+ yacht,
+ currentUserId,
+}: {
+ yachtId: string;
+ yacht: YachtTabsYacht;
+ currentUserId?: string;
+}) {
const mutation = useYachtPatch(yachtId);
const save =
(field: YachtPatchField, transform?: (v: string | null) => string | number | null) =>
@@ -224,14 +232,17 @@ function OverviewTab({ yachtId, yacht }: { yachtId: string; yacht: YachtTabsYach
- {/* Notes */}
+ {/* Notes — threaded list (parity with clients/interests/companies).
+ The legacy single-field `yacht.notes` column stays in schema
+ for the EOI/contract merge-field path; OverviewTab no longer
+ exposes it for editing here. */}
Notes
-
@@ -327,7 +338,7 @@ export function getYachtTabs({ yachtId, currentUserId, yacht }: YachtTabsOptions
{
id: 'overview',
label: 'Overview',
- content: ,
+ content: ,
},
{
id: 'ownership-history',
diff --git a/src/lib/services/documents.service.ts b/src/lib/services/documents.service.ts
index ae27a524..803bcb87 100644
--- a/src/lib/services/documents.service.ts
+++ b/src/lib/services/documents.service.ts
@@ -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 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,