diff --git a/src/components/clients/client-form.tsx b/src/components/clients/client-form.tsx index 8e85768d..0fed2fb6 100644 --- a/src/components/clients/client-form.tsx +++ b/src/components/clients/client-form.tsx @@ -251,23 +251,12 @@ export function ClientForm({ Basic Information -
-
- - - {errors.fullName && ( -

{errors.fullName.message}

- )} -
- -
- - setValue('nationalityIso', iso ?? undefined)} - data-testid="client-nationality" - /> -
+
+ + + {errors.fullName && ( +

{errors.fullName.message}

+ )}
@@ -450,6 +439,14 @@ export function ClientForm({ +
+ + setValue('nationalityIso', iso ?? undefined)} + data-testid="client-nationality" + /> +
(null); + // F23: when the rep picks a non-Enquiry stage without a yacht linked, + // we drop into a yacht-picker view in the popover. yachtPrereqTarget + // holds the stage they were trying to reach; submitting the picker + // PATCHes the yachtId, then chains the original stage move. + const [yachtPrereqTarget, setYachtPrereqTarget] = useState(null); + const [yachtPrereqId, setYachtPrereqId] = useState(null); + const [linkingYacht, setLinkingYacht] = useState(false); // When a user picks a stage that isn't a legal next step (and has the // override permission), the popover transitions into a confirm view // that asks for a reason before committing. Reasons are not exposed @@ -130,6 +148,15 @@ export function InlineStagePicker({ setOpen(false); return; } + // F23: leaving Enquiry without a yacht linked is the one hard prereq + // the service enforces. Instead of bouncing them to the validation + // error toast, drop into an inline yacht-picker so the rep can fix + // it in place. Same flow chains the stage move after the link. + if (stage === 'enquiry' && next !== 'enquiry' && !currentYachtId) { + setYachtPrereqTarget(next); + setYachtPrereqId(null); + return; + } const isOverride = !canTransitionStage(stage, next); if (isOverride && canOverride) { // Switch into the confirm view rather than firing the mutation @@ -143,6 +170,33 @@ export function InlineStagePicker({ mutation.mutate({ next, reason: null }); } + async function commitYachtPrereq() { + if (!yachtPrereqTarget || !yachtPrereqId) return; + setLinkingYacht(true); + try { + await apiFetch(`/api/v1/interests/${interestId}`, { + method: 'PATCH', + body: { yachtId: yachtPrereqId }, + }); + queryClient.invalidateQueries({ queryKey: ['interests', interestId] }); + queryClient.invalidateQueries({ queryKey: ['interests'] }); + const target = yachtPrereqTarget; + setYachtPrereqTarget(null); + setYachtPrereqId(null); + setPendingStage(target); + mutation.mutate({ next: target, reason: null }); + } catch (err) { + toastError(err); + } finally { + setLinkingYacht(false); + } + } + + function cancelYachtPrereq() { + setYachtPrereqTarget(null); + setYachtPrereqId(null); + } + async function unlinkAllAndOpen(target: PipelineStage) { setUnlinking(true); try { @@ -196,9 +250,12 @@ export function InlineStagePicker({ { - if (mutation.isPending) return; + if (mutation.isPending || linkingYacht) return; setOpen(o); - if (!o) cancelOverride(); + if (!o) { + cancelOverride(); + cancelYachtPrereq(); + } }} > @@ -228,7 +285,57 @@ export function InlineStagePicker({ className="w-72 p-0" onClick={(e) => stopPropagation && e.stopPropagation()} > - {overrideTarget ? ( + {yachtPrereqTarget ? ( + // F23: inline yacht-prereq view — only reached when the rep + // picked a non-Enquiry stage without a yacht linked. Surfaces + // a yacht-picker right inside the popover so they can fix + // the prereq and move the stage in one flow. +
+
+ +
+

Yacht required

+

+ A yacht must be linked before leaving Enquiry. Pick one below to move to{' '} + {STAGE_LABELS[yachtPrereqTarget]}. +

+
+
+
+ + setYachtPrereqId(id)} + ownerFilter={clientId ? { type: 'client', id: clientId } : undefined} + disabled={linkingYacht || mutation.isPending} + /> +
+
+ + +
+
+ ) : overrideTarget ? ( // Confirm-override view: only reached when the user picked a // stage that isn't a legal next step. Reason is optional but // strongly nudged for the audit log. diff --git a/src/components/interests/interest-detail-header.tsx b/src/components/interests/interest-detail-header.tsx index 372dcc5c..f6c09725 100644 --- a/src/components/interests/interest-detail-header.tsx +++ b/src/components/interests/interest-detail-header.tsx @@ -80,6 +80,7 @@ interface InterestDetailHeaderProps { activeReminderCount?: number; berthId: string | null; berthMooringNumber: string | null; + yachtId: string | null; pipelineStage: string; leadCategory: string | null; source: string | null; @@ -243,6 +244,8 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade )} diff --git a/src/components/interests/interest-detail.tsx b/src/components/interests/interest-detail.tsx index cc5ea135..a26da936 100644 --- a/src/components/interests/interest-detail.tsx +++ b/src/components/interests/interest-detail.tsx @@ -43,6 +43,9 @@ interface InterestData { } | null; berthId: string | null; berthMooringNumber: string | null; + /** Linked yacht — null until the rep ties one to the deal. Required to + * leave Enquiry; surfaced inline in the stage picker as a prereq. */ + yachtId: string | null; /** Yacht-fit dimensions (numeric strings from postgres). Drive the * recommender panel guard ("Set desired dimensions to see recommendations"). */ desiredLengthFt: string | null;