Files
campaign-command-center/src/components/ui/NextBestAction.tsx
Chris Olson d2d5632086 Implement Phase 2 features for Campaign Command Center
Adds 14 new features across the dashboard:
- Deal Risk Scoring (0-100) with risk factors on pipeline deals
- Account Health Score with multi-factor analysis on account cards
- Next Best Action Engine on Executive Overview
- Account Notes with pin/edit/delete in activity overlay and account detail
- Stakeholder Contact Map with role, sentiment, email, LinkedIn
- Competitive Intelligence Tracker with multi-select competitors and filtering
- CSV/Excel Export on accounts, activities, and pipeline pages
- Data Quality Dashboard in admin with completeness scoring
- Campaign Playbook Templates with 5 default plays and account progress tracking
- Goals & District Targets with quarterly tracking and progress bars
- Weekly Status Report (printable) and Executive Summary (printable)
- Activity Effectiveness Scoring showing pipeline conversion by activity type
- Mobile Activity FAB for quick activity logging on mobile
- Multi-User Auth (NextAuth + Google OAuth, @broadcom.com domain, role-based)

Also adds Phase 2 API route, scoring utilities, sidebar navigation updates,
and database schema extensions for notes, contacts, snapshots, playbooks,
and district targets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-31 18:07:46 -04:00

149 lines
5.7 KiB
TypeScript

'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 (
<div className="text-center py-6 text-sm text-muted">
No immediate actions needed territory is well-covered.
</div>
);
}
return (
<div className="space-y-2">
{actions.map((action, i) => (
<div key={i} className={`border-l-3 rounded-lg p-3 ${PRIORITY_STYLES[action.priority]}`}>
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-xs font-bold text-foreground truncate">{action.account}</span>
<span className={`px-1.5 py-0.5 rounded text-[9px] font-semibold ${PRIORITY_LABELS[action.priority].color}`}>
{PRIORITY_LABELS[action.priority].text}
</span>
</div>
<div className="text-xs font-medium text-foreground">{action.message}</div>
<div className="text-[11px] text-muted mt-0.5">{action.detail}</div>
</div>
</div>
</div>
))}
</div>
);
}