diff --git a/src/components/interests/interest-tabs.tsx b/src/components/interests/interest-tabs.tsx
index 96218a16..1145d9bf 100644
--- a/src/components/interests/interest-tabs.tsx
+++ b/src/components/interests/interest-tabs.tsx
@@ -19,6 +19,7 @@ import {
AccordionTrigger,
} from '@/components/ui/accordion';
import { NotesList } from '@/components/shared/notes-list';
+import { ProxyCard } from '@/components/shared/proxy-card';
import { InlineEditableField } from '@/components/shared/inline-editable-field';
import { FieldHistoryProvider, FieldHistoryIcon } from '@/components/shared/field-history';
import { ClientChannelEditor } from '@/components/clients/client-channel-editor';
@@ -1133,6 +1134,9 @@ function OverviewTab({
archivedAt={null}
/>
+ {/* CM-9: per-deal point-of-contact (overrides the client's default). */}
+
+
{/* Qualification checklist - surfaces the port's per-port criteria so
the rep can mark each one confirmed before the deal advances out
of 'enquiry'. Hidden when the port has no enabled criteria. */}
diff --git a/src/components/shared/proxy-card.tsx b/src/components/shared/proxy-card.tsx
new file mode 100644
index 00000000..9c914437
--- /dev/null
+++ b/src/components/shared/proxy-card.tsx
@@ -0,0 +1,249 @@
+'use client';
+
+import { useState } from 'react';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { Mail, Phone, UserCheck, UserPlus } from 'lucide-react';
+import { toast } from 'sonner';
+
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { usePermissions } from '@/hooks/use-permissions';
+import { apiFetch } from '@/lib/api/client';
+import { toastError } from '@/lib/api/toast-error';
+
+type ProxyEntityType = 'client' | 'interest' | 'yacht';
+
+interface Proxy {
+ id: string;
+ name: string;
+ email: string | null;
+ phone: string | null;
+ relationship: string | null;
+ notes: string | null;
+}
+
+const RESOURCE: Record
= {
+ client: 'clients',
+ interest: 'interests',
+ yacht: 'yachts',
+};
+
+/**
+ * CM-9: point-of-contact ("proxy") panel for a client / interest / yacht detail
+ * page. Reads + edits the per-entity proxy via the entity's sub-resource route.
+ */
+export function ProxyCard({
+ entityType,
+ entityId,
+}: {
+ entityType: ProxyEntityType;
+ entityId: string;
+}) {
+ const { can } = usePermissions();
+ const canManage = can(RESOURCE[entityType], 'edit');
+ const qc = useQueryClient();
+ const base = `/api/v1/${RESOURCE[entityType]}/${entityId}/proxy`;
+ const queryKey = ['proxy', entityType, entityId];
+
+ const { data } = useQuery<{ data: Proxy | null }>({
+ queryKey,
+ queryFn: () => apiFetch(base),
+ });
+ const proxy = data?.data ?? null;
+
+ const [open, setOpen] = useState(false);
+
+ const remove = useMutation({
+ mutationFn: () => apiFetch(base, { method: 'DELETE' }),
+ onSuccess: () => {
+ toast.success('Point of contact removed');
+ qc.invalidateQueries({ queryKey });
+ },
+ onError: (err) => toastError(err),
+ });
+
+ return (
+
+
+
+
+ Point of contact
+
+ {canManage ? (
+
+ ) : null}
+
+
+ {proxy ? (
+
+
+ {proxy.name}
+ {proxy.relationship ? (
+
+ {proxy.relationship}
+
+ ) : null}
+
+ {proxy.email ? (
+
+
+ {proxy.email}
+
+ ) : null}
+ {proxy.phone ? (
+
+
+ {proxy.phone}
+
+ ) : null}
+ {proxy.notes ?
{proxy.notes}
: null}
+ {canManage ? (
+
+ ) : null}
+
+ ) : (
+
+ No proxy set — comms go to the {entityType} directly.
+
+ )}
+
+ {open ? (
+
qc.invalidateQueries({ queryKey })}
+ />
+ ) : null}
+
+ );
+}
+
+function ProxyDialog({
+ open,
+ onOpenChange,
+ base,
+ existing,
+ entityType,
+ onSaved,
+}: {
+ open: boolean;
+ onOpenChange: (v: boolean) => void;
+ base: string;
+ existing: Proxy | null;
+ entityType: ProxyEntityType;
+ onSaved: () => void;
+}) {
+ const [name, setName] = useState(existing?.name ?? '');
+ const [email, setEmail] = useState(existing?.email ?? '');
+ const [phone, setPhone] = useState(existing?.phone ?? '');
+ const [relationship, setRelationship] = useState(existing?.relationship ?? '');
+ const [notes, setNotes] = useState(existing?.notes ?? '');
+ // State seeds from `existing` at mount; the dialog is remounted on each open
+ // (the parent renders it conditionally), so no reseed effect is needed.
+
+ const save = useMutation({
+ mutationFn: () =>
+ apiFetch(base, {
+ method: 'PUT',
+ body: { name: name.trim(), email, phone, relationship, notes },
+ }),
+ onSuccess: () => {
+ toast.success('Point of contact saved');
+ onSaved();
+ onOpenChange(false);
+ },
+ onError: (err) => toastError(err),
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/yachts/yacht-tabs.tsx b/src/components/yachts/yacht-tabs.tsx
index 0d6ab1b9..538dfca6 100644
--- a/src/components/yachts/yacht-tabs.tsx
+++ b/src/components/yachts/yacht-tabs.tsx
@@ -12,6 +12,7 @@ import { TenancyCreateDialog } from '@/components/tenancies/tenancy-create-dialo
import { InlineEditableField } from '@/components/shared/inline-editable-field';
import { FieldHistoryProvider, FieldHistoryIcon } from '@/components/shared/field-history';
import { InlineTagEditor } from '@/components/shared/inline-tag-editor';
+import { ProxyCard } from '@/components/shared/proxy-card';
import { NotesList } from '@/components/shared/notes-list';
import { EntityActivityFeed } from '@/components/shared/entity-activity-feed';
import { TenancyList, type TenancyRow } from '@/components/tenancies/tenancy-list';
@@ -176,6 +177,10 @@ function OverviewTab({
return (
+ {/* CM-9: per-vessel point-of-contact (overrides interest + client). */}
+
{/* Identity */}
Identity