'use client'; import { useMemo } from 'react'; import { AccountRecord, ActivityRecord, PipelineRecord } from '@/types/data'; import { differenceInDays, parseISO, format } from 'date-fns'; interface ActionItem { priority: 'urgent' | 'high' | 'medium'; account: string; message: string; detail: string; type: 'stale' | 'renewal' | 'pipeline_gap' | 'deal_risk' | 'untouched'; } const PRIORITY_STYLES = { urgent: 'border-l-red-500 bg-red-50', high: 'border-l-amber-500 bg-amber-50', medium: 'border-l-blue-500 bg-blue-50', }; const PRIORITY_LABELS = { urgent: { text: 'Urgent', color: 'text-red-600 bg-red-100' }, high: { text: 'Action Needed', color: 'text-amber-700 bg-amber-100' }, medium: { text: 'Consider', color: 'text-blue-600 bg-blue-100' }, }; export function NextBestAction({ accounts, activities, pipeline, limit = 8 }: { accounts: AccountRecord[]; activities: ActivityRecord[]; pipeline: PipelineRecord[]; limit?: number; }) { const actions = useMemo(() => { const items: ActionItem[] = []; const now = new Date(); for (const acct of accounts) { if (acct.Priority !== 'High' && acct.Priority !== 'Medium') continue; const acctActivities = activities.filter(a => a.Account_Name === acct.Account_Name); const acctPipeline = pipeline.filter(p => p.Account_Name === acct.Account_Name && p.Stage !== '06-Closed Won' && p.Stage !== '07-Closed Lost'); // Stale high-priority accounts if (acct.Priority === 'High') { if (acctActivities.length === 0) { items.push({ priority: 'urgent', account: acct.Account_Name, message: 'High-priority account with zero touches', detail: `${acct.Tier} account, ${acct.Current_ARR_USD ? `$${Math.round(acct.Current_ARR_USD / 1000)}K ARR` : 'no ARR recorded'}`, type: 'untouched', }); } else { const lastDate = acctActivities.sort((a, b) => b.Activity_Date.localeCompare(a.Activity_Date))[0]; const daysSince = differenceInDays(now, parseISO(lastDate.Activity_Date)); if (daysSince > 30) { items.push({ priority: daysSince > 60 ? 'urgent' : 'high', account: acct.Account_Name, message: `No activity in ${daysSince} days`, detail: `Last touch: ${format(parseISO(lastDate.Activity_Date), 'MMM d')} (${lastDate.Activity_Type})`, type: 'stale', }); } } } // Approaching renewals with no play if (acct.Next_Renewal_Date) { const daysToRenewal = differenceInDays(parseISO(acct.Next_Renewal_Date), now); if (daysToRenewal > 0 && daysToRenewal <= 120 && acctPipeline.length === 0) { items.push({ priority: daysToRenewal <= 60 ? 'urgent' : 'high', account: acct.Account_Name, message: `Renewal in ${daysToRenewal} days, no pipeline`, detail: `${acct.Next_Renewal_EAR ? `$${Math.round(acct.Next_Renewal_EAR / 1000)}K EAR` : 'EAR not set'} — start attach/renewal play`, type: 'renewal', }); } } // High-priority with no pipeline if (acct.Priority === 'High' && acctPipeline.length === 0 && acctActivities.length > 0) { const lastDate = acctActivities.sort((a, b) => b.Activity_Date.localeCompare(a.Activity_Date))[0]; const daysSince = differenceInDays(now, parseISO(lastDate.Activity_Date)); if (daysSince <= 30) { items.push({ priority: 'medium', account: acct.Account_Name, message: 'Active engagement but no pipeline', detail: `${acctActivities.length} touches — consider creating an opportunity`, type: 'pipeline_gap', }); } } } // Deal risks — overdue close dates const openDeals = pipeline.filter(p => p.Stage !== '06-Closed Won' && p.Stage !== '07-Closed Lost'); for (const deal of openDeals) { if (deal.Expected_Close_Date) { const daysOverdue = differenceInDays(now, parseISO(deal.Expected_Close_Date)); if (daysOverdue > 14) { items.push({ priority: daysOverdue > 30 ? 'urgent' : 'high', account: deal.Account_Name, message: `Deal close date ${daysOverdue} days overdue`, detail: `$${Math.round(deal.Amount_USD / 1000)}K — update close date or advance stage`, type: 'deal_risk', }); } } } // Sort by priority const order = { urgent: 0, high: 1, medium: 2 }; return items.sort((a, b) => order[a.priority] - order[b.priority]).slice(0, limit); }, [accounts, activities, pipeline, limit]); if (actions.length === 0) { return (
No immediate actions needed — territory is well-covered.
); } return (
{actions.map((action, i) => (
{action.account} {PRIORITY_LABELS[action.priority].text}
{action.message}
{action.detail}
))}
); }