diff --git a/src/components/clients/client-tabs.tsx b/src/components/clients/client-tabs.tsx index 90c9cb4e..e02efbd3 100644 --- a/src/components/clients/client-tabs.tsx +++ b/src/components/clients/client-tabs.tsx @@ -11,6 +11,7 @@ import { RemindersInline } from '@/components/reminders/reminders-inline'; import { primaryTimezoneFor } from '@/lib/i18n/timezones'; import { InlineTagEditor } from '@/components/shared/inline-tag-editor'; import { NotesList } from '@/components/shared/notes-list'; +import { ProxyCard } from '@/components/shared/proxy-card'; import type { CountryCode } from '@/lib/i18n/countries'; import { ClientInterestsTab } from '@/components/clients/client-interests-tab'; import { ClientPipelineSummary } from '@/components/clients/client-pipeline-summary'; @@ -156,6 +157,9 @@ function OverviewTab({ + {/* CM-9: point-of-contact (default level for the client). */} + +
{/* Personal Info */}
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 ( + + + + Point of contact + + A person who acts as the point of contact for this {entityType}. Used to address + outbound comms. + + +
+
+ + setName(e.target.value)} + autoFocus + /> +
+
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPhone(e.target.value)} /> +
+
+
+ + setRelationship(e.target.value)} + /> +
+
+ + setNotes(e.target.value)} /> +
+
+ + + + +
+
+ ); +} 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