'use client'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { format, formatDistanceToNowStrict } from 'date-fns'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { CheckCircle2, Circle, FileSignature, Plus, Send, Wallet } from 'lucide-react'; import type { DetailTab } from '@/components/shared/detail-layout'; import { Button } from '@/components/ui/button'; import { NotesList } from '@/components/shared/notes-list'; import { InlineEditableField } from '@/components/shared/inline-editable-field'; import { InlineTagEditor } from '@/components/shared/inline-tag-editor'; import { RecommendationList } from '@/components/interests/recommendation-list'; import { InterestTimeline } from '@/components/interests/interest-timeline'; import { InterestDocumentsTab } from '@/components/interests/interest-documents-tab'; import { InterestFilesTab } from '@/components/interests/interest-files-tab'; import { LEAD_CATEGORIES, PIPELINE_STAGES, type PipelineStage } from '@/lib/constants'; import { apiFetch } from '@/lib/api/client'; import { cn } from '@/lib/utils'; type InterestPatchField = 'leadCategory' | 'source' | 'notes'; const LEAD_CATEGORY_OPTIONS = LEAD_CATEGORIES.map((c) => ({ value: c, label: c.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()), })); // Convert raw enum values like `waiting_for_signatures` → `Waiting For Signatures`. function humanizeStatus(value: string | null): string | null { if (!value) return null; return value.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()); } interface InterestTabsOptions { interestId: string; currentUserId?: string; interest: { pipelineStage: string; leadCategory: string | null; source: string | null; eoiStatus: string | null; contractStatus: string | null; depositStatus: string | null; reservationStatus: string | null; dateFirstContact: string | null; dateLastContact: string | null; dateEoiSent: string | null; dateEoiSigned: string | null; dateContractSent: string | null; dateContractSigned: string | null; dateDepositReceived: string | null; reminderEnabled: boolean; reminderDays: number | null; reminderLastFired: string | null; notes: string | null; /** Surfaced by getInterestById for the Overview "most recent note" * teaser - saves a click into the Notes tab to peek at the latest. */ notesCount?: number; recentNote?: { id: string; content: string; authorId: string; createdAt: string; } | null; tags?: Array<{ id: string; name: string; color: string }>; }; } function useInterestPatch(interestId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: async (patch: Partial>) => apiFetch(`/api/v1/interests/${interestId}`, { method: 'PATCH', body: patch, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['interests', interestId] }); }, }); } function useStageMutation(interestId: string) { const qc = useQueryClient(); return useMutation({ mutationFn: async ({ stage, reason }: { stage: string; reason?: string }) => apiFetch(`/api/v1/interests/${interestId}/stage`, { method: 'PATCH', body: { pipelineStage: stage, reason }, }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['interests', interestId] }); qc.invalidateQueries({ queryKey: ['interests'] }); }, }); } function EditableRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function InfoRow({ label, value }: { label: string; value?: string | null }) { if (!value) return null; return (
{label}
{value}
); } function formatDate(date: string | null) { if (!date) return null; return format(new Date(date), 'MMM d, yyyy'); } function relativeDate(date: string | null) { if (!date) return null; return `${formatDistanceToNowStrict(new Date(date))} ago`; } interface MilestoneSectionProps { title: string; icon: React.ComponentType<{ className?: string }>; /** Lifecycle for this milestone, in chronological order. */ steps: Array<{ label: string; date: string | null; /** Stage to advance to when the user clicks the action button for this step. */ advanceStage?: string; /** Optional override for the action label. */ actionLabel?: string; /** Suppress the inline "Mark as…" button for this step. Use when the * parent supplies a richer CTA via `footer` (e.g. Deposit, where we * want the invoice flow to be the primary path). */ hideAutoButton?: boolean; }>; status: string | null; onAdvance: (stage: string) => void; isPending: boolean; /** Current pipelineStage. Used to mark steps as done when the pipeline has * moved past their advanceStage even if the date stamp is missing - e.g. * a seed-data interest that started already at eoi_signed will show both * EOI sub-steps as done. Stage truth > date truth. */ currentStage: string; /** When true, this milestone is the next one the user should act on: * card gets a brand-accent ring and the next-step CTA becomes a primary * button. Computed by the parent based on currentStage. */ isActive?: boolean; /** Extra nodes (e.g. "Create deposit invoice" link) rendered below the steps. */ footer?: React.ReactNode; } /** * One milestone section (EOI / Deposit / Contract) - shows a vertical lifecycle * with completed steps checked, the next step exposing a quick "mark as…" * button that bumps the pipeline stage. Each stage flip auto-stamps its date * via the service layer (interests.service.ts). When external systems wire in * (Documenso webhook, paid invoice → deposit, etc.), they patch the same * stage endpoint and these checkmarks light up automatically. */ function MilestoneSection({ title, icon: Icon, steps, status, onAdvance, isPending, currentStage, isActive, footer, }: MilestoneSectionProps) { const currentStageIdx = PIPELINE_STAGES.indexOf(currentStage as PipelineStage); // A step counts as done if either: // (a) its `advanceStage` is at or behind the current pipeline stage, OR // (b) it has an explicit date stamp (from a manual mark or webhook). // (a) handles seeded/imported interests that arrived at a later stage // without per-step dates. const doneFlags = steps.map((step) => { if (step.date) return true; if (!step.advanceStage) return false; const stepIdx = PIPELINE_STAGES.indexOf(step.advanceStage as PipelineStage); return stepIdx !== -1 && currentStageIdx !== -1 && currentStageIdx >= stepIdx; }); const firstUnsetIdx = doneFlags.findIndex((d) => !d); return (

{title}

{isActive ? ( Next ) : null}
{status ? ( {humanizeStatus(status)} ) : null}
    {steps.map((step, i) => { const done = doneFlags[i] ?? false; const isNext = !done && i === firstUnsetIdx; return (
  1. {done ? ( ) : ( )}
    {step.label} {step.date ? ( {formatDate(step.date)} · {relativeDate(step.date)} ) : null}
    {isNext && step.advanceStage && !step.hideAutoButton ? ( ) : null}
  2. ); })}
{footer ?
{footer}
: null}
); } function OverviewTab({ interestId, interest, }: { interestId: string; interest: InterestTabsOptions['interest']; }) { const params = useParams<{ portSlug: string }>(); const portSlug = params?.portSlug ?? ''; const mutation = useInterestPatch(interestId); const stageMutation = useStageMutation(interestId); const save = (field: InterestPatchField) => async (next: string | null) => { await mutation.mutateAsync({ [field]: next }); }; const advance = (stage: string) => stageMutation.mutate({ stage, reason: 'Marked from overview' }); // Which milestone is the next one to act on? "EOI Signed" → Deposit is next; // "Deposit 10%" → Contract is next; "Contract Signed" / "Completed" → none. const stageIdx = PIPELINE_STAGES.indexOf(interest.pipelineStage as PipelineStage); const eoiSignedIdx = PIPELINE_STAGES.indexOf('eoi_signed'); const depositIdx = PIPELINE_STAGES.indexOf('deposit_10pct'); const contractSignedIdx = PIPELINE_STAGES.indexOf('contract_signed'); let activeMilestone: 'eoi' | 'deposit' | 'contract' | null = null; if (stageIdx === -1 || stageIdx >= contractSignedIdx) { activeMilestone = null; } else if (stageIdx < eoiSignedIdx) { activeMilestone = 'eoi'; } else if (stageIdx < depositIdx) { activeMilestone = 'deposit'; } else { activeMilestone = 'contract'; } return (
{/* Sales-process milestones - the heart of the system. Each section is a mini lifecycle that auto-completes as actions happen on the platform (Documenso webhook, paid deposit invoice, signed contract). Until the automation lands, salespeople nudge stages forward via the inline buttons here, which auto-stamp the milestone date server-side. */}
) : null } />
{/* Lead & Source (editable) */}

Lead

{/* Contact dates (read-only - kept compact next to Lead) */}

Contact

{interest.reservationStatus ? ( ) : null}
{/* Reminder */} {interest.reminderEnabled && (

Reminder

)} {/* Most-recent threaded note teaser. Saves a click into the Notes tab when the rep just wants to peek at "what was discussed last." Hidden when there's nothing to show. */} {interest.recentNote ? (

Latest note

View all {interest.notesCount && interest.notesCount > 1 ? ` ${interest.notesCount}` : ''}

{interest.recentNote.content}

{formatDistanceToNowStrict(new Date(interest.recentNote.createdAt), { addSuffix: true, })} {interest.recentNote.authorId ? ` · ${interest.recentNote.authorId === 'system' ? 'system' : interest.recentNote.authorId}` : ''}

) : null} {/* Notes (editable, multiline) */}

Notes

{/* Tags */}

Tags

); } export function getInterestTabs({ interestId, currentUserId, interest, }: InterestTabsOptions): DetailTab[] { return [ { id: 'overview', label: 'Overview', content: , }, { id: 'notes', label: 'Notes', content: ( ), }, { id: 'documents', label: 'Documents', content: , }, { id: 'files', label: 'Files', content: , }, { id: 'recommendations', label: 'Recommendations', content: , }, { id: 'activity', label: 'Activity', content: , }, ]; }