diff --git a/src/components/interests/interest-tabs.tsx b/src/components/interests/interest-tabs.tsx index 093f223a..f211e309 100644 --- a/src/components/interests/interest-tabs.tsx +++ b/src/components/interests/interest-tabs.tsx @@ -63,7 +63,13 @@ type InterestPatchField = | 'source' | 'desiredLengthFt' | 'desiredWidthFt' - | 'desiredDraftFt'; + | 'desiredDraftFt' + | 'dateEoiSent' + | 'dateEoiSigned' + | 'dateReservationSigned' + | 'dateDepositReceived' + | 'dateContractSent' + | 'dateContractSigned'; const LEAD_CATEGORY_OPTIONS = LEAD_CATEGORIES.map((c) => ({ value: c, @@ -347,6 +353,72 @@ function MilestoneAdvanceButton({ ); } +/** + * Skip-ahead backfill control: shown next to past milestones whose + * date column is null. Opens the same date popover as + * MilestoneAdvanceButton but PATCHes the date column directly without + * triggering a stage transition — the stage was already advanced + * manually upstream. + */ +function MilestoneBackfillButton({ + label, + disabled, + onConfirm, +}: { + label: string; + disabled?: boolean; + onConfirm: (milestoneDate: string) => void | Promise; +}) { + const [open, setOpen] = useState(false); + const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10)); + return ( + + + + + +
+ + +

+ Records the date the milestone happened. Does not change the deal's pipeline stage. +

+
+
+ + +
+
+
+ ); +} + function MilestoneSection({ title, icon: Icon, @@ -616,10 +688,37 @@ function OverviewTab({ } as const; const order = ['berth_interest', 'eoi', 'reservation', 'deposit', 'contract'] as const; const firstIncompleteKey = order.find((k) => !milestoneCompletion[k]) ?? null; + // 2026-05-21: a "stage-owning" milestone is the one that maps 1:1 with + // the deal's current pipelineStage column. When the rep manually + // jumps the stage forward (Reservation+) but earlier sub-statuses + // are still un-signed, we need the current-stage milestone to stay + // marked `'current'` regardless of completion — otherwise EOI gets + // flagged as NEXT STEP and the actual current stage hides under + // "Upcoming milestones". Earlier-than-stage milestones go to + // `'past'` so the rep can render backfill controls against them. + const STAGE_TO_MILESTONE: Partial> = { + eoi: 'eoi', + reservation: 'reservation', + deposit_paid: 'deposit', + contract: 'contract', + }; + const stageOwnedMilestone = STAGE_TO_MILESTONE[interest.pipelineStage as PipelineStage] ?? null; + const stageOwnedIdx = stageOwnedMilestone ? order.indexOf(stageOwnedMilestone) : -1; const phaseFor = (k: (typeof order)[number]): Phase => { - if (milestoneCompletion[k]) return 'past'; - if (k === firstIncompleteKey) return 'current'; - return 'future'; + // Stage owns this milestone → always current, never collapsed. + if (stageOwnedMilestone === k) return 'current'; + // Otherwise: completion drives past/future ordering when no stage + // owns a milestone (enquiry/qualified/nurturing stages). + if (!stageOwnedMilestone) { + if (milestoneCompletion[k]) return 'past'; + if (k === firstIncompleteKey) return 'current'; + return 'future'; + } + // A stage DOES own a different milestone — bucket by position + // relative to it. Earlier slots go to `past` even if incomplete + // (the backfill controls live there); later slots go to `future`. + const idx = order.indexOf(k); + return idx < stageOwnedIdx ? 'past' : 'future'; }; const berthInterestPhase: Phase = phaseFor('berth_interest'); const eoiPhase: Phase = phaseFor('eoi'); @@ -730,9 +829,20 @@ function OverviewTab({ ) : null, - pastSummary: interest.dateEoiSigned - ? `Signed ${formatDate(interest.dateEoiSigned)}` - : 'Completed', + pastSummary: interest.dateEoiSigned ? ( + `Signed ${formatDate(interest.dateEoiSigned)}` + ) : ( + + Completed + { + await mutation.mutateAsync({ dateEoiSigned: d }); + }} + /> + + ), }, { key: 'reservation', @@ -748,9 +858,20 @@ function OverviewTab({ actionLabel: 'Mark reservation as signed', }, ], - pastSummary: interest.dateReservationSigned - ? `Signed ${formatDate(interest.dateReservationSigned)}` - : 'Completed', + pastSummary: interest.dateReservationSigned ? ( + `Signed ${formatDate(interest.dateReservationSigned)}` + ) : ( + + Completed + { + await mutation.mutateAsync({ dateReservationSigned: d }); + }} + /> + + ), }, { key: 'deposit', @@ -780,9 +901,20 @@ function OverviewTab({ ) : null, - pastSummary: interest.dateDepositReceived - ? `Received ${formatDate(interest.dateDepositReceived)}` - : 'Recorded', + pastSummary: interest.dateDepositReceived ? ( + `Received ${formatDate(interest.dateDepositReceived)}` + ) : ( + + Recorded + { + await mutation.mutateAsync({ dateDepositReceived: d }); + }} + /> + + ), }, { key: 'contract', @@ -804,9 +936,20 @@ function OverviewTab({ actionLabel: 'Mark contract as signed', }, ], - pastSummary: interest.dateContractSigned - ? `Signed ${formatDate(interest.dateContractSigned)}` - : 'Completed', + pastSummary: interest.dateContractSigned ? ( + `Signed ${formatDate(interest.dateContractSigned)}` + ) : ( + + Completed + { + await mutation.mutateAsync({ dateContractSigned: d }); + }} + /> + + ), }, ]; diff --git a/src/lib/validators/interests.ts b/src/lib/validators/interests.ts index bdb85198..3e4b5bb6 100644 --- a/src/lib/validators/interests.ts +++ b/src/lib/validators/interests.ts @@ -63,6 +63,7 @@ export const createInterestSchema = z.object({ dateEoiSent: z.coerce.date().nullable().optional(), dateEoiSigned: z.coerce.date().nullable().optional(), dateReservationSigned: z.coerce.date().nullable().optional(), + dateDepositReceived: z.coerce.date().nullable().optional(), dateContractSent: z.coerce.date().nullable().optional(), dateContractSigned: z.coerce.date().nullable().optional(), pipelineStage: z.enum(PIPELINE_STAGES).default('enquiry'),