'use client'; import { useState, useEffect, useMemo, useCallback } from 'react'; import { PageHeader } from '@/components/ui/PageHeader'; import { ChartCard } from '@/components/ui/ChartCard'; import { Scorecard } from '@/components/ui/Scorecard'; import { formatCurrency, formatPercent, formatNumber, CHART_COLORS, CHART_PALETTE, DISTRICT_SHORT } from '@/lib/formatters'; import { useData } from '@/lib/data-context'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell, } from 'recharts'; interface Playbook { id: string; play_name: string; description: string; steps_json: string; } interface PlaybookProgress { id: string; Account_Name: string; playbook_id: string; current_step: string; status: string; started_at: string; updated_at: string; } function parseSteps(playbook: Playbook): string[] { try { const parsed = JSON.parse(playbook.steps_json); if (Array.isArray(parsed)) { return parsed.map((s: string | { name?: string; title?: string }) => typeof s === 'string' ? s : s.name ?? s.title ?? String(s) ); } return []; } catch { return []; } } function daysBetween(a: string, b: string): number { const ms = new Date(b).getTime() - new Date(a).getTime(); return Math.max(0, Math.round(ms / (1000 * 60 * 60 * 24))); } export default function PlaybookEffectivenessPage() { const { filtered } = useData(); const [playbooks, setPlaybooks] = useState([]); const [progress, setProgress] = useState([]); const [loading, setLoading] = useState(true); const [selectedPlayId, setSelectedPlayId] = useState(''); const [comparePlayId, setComparePlayId] = useState(''); useEffect(() => { (async () => { setLoading(true); try { const [pbRes, prRes] = await Promise.all([ fetch('/api/phase2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'playbooks.list' }), }), fetch('/api/phase2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'playbooks.progress.list', Account_Name: '__all__' }), }), ]); const pbJson = await pbRes.json(); const prJson = await prRes.json(); const pbs: Playbook[] = pbJson.data ?? []; setPlaybooks(pbs); setProgress(prJson.data ?? []); if (pbs.length > 0) setSelectedPlayId(pbs[0].id); } catch { setPlaybooks([]); setProgress([]); } finally { setLoading(false); } })(); }, []); const selectedPlaybook = useMemo(() => playbooks.find(p => p.id === selectedPlayId), [playbooks, selectedPlayId]); const comparePlaybook = useMemo(() => playbooks.find(p => p.id === comparePlayId), [playbooks, comparePlayId]); // Build metrics for a given playbook const getPlayMetrics = useCallback((playId: string) => { const play = playbooks.find(p => p.id === playId); if (!play) return null; const playProgress = progress.filter(p => p.playbook_id === playId); const accountNames = new Set(playProgress.map(p => p.Account_Name)); const assigned = playProgress.length; const active = playProgress.filter(p => p.status === 'in_progress').length; const completed = playProgress.filter(p => p.status === 'completed').length; // Pipeline from accounts in this play const playPipeline = filtered.pipeline.filter( d => accountNames.has(d.Account_Name) || d.Source_Play === play.play_name ); const pipelineGenerated = playPipeline.reduce((s, d) => s + d.Amount_USD, 0); const closedWon = playPipeline .filter(d => d.Stage === '06-Closed Won') .reduce((s, d) => s + (d.Closed_Amount_USD ?? d.Amount_USD), 0); const conversionRate = assigned > 0 ? (playPipeline.filter(d => d.Stage === '06-Closed Won').length / assigned) * 100 : 0; // Average time in play for completed const completedProgress = playProgress.filter(p => p.status === 'completed'); const avgDays = completedProgress.length > 0 ? completedProgress.reduce((s, p) => s + daysBetween(p.started_at, p.updated_at), 0) / completedProgress.length : 0; // Step distribution const steps = parseSteps(play); const stepCounts: Record = {}; steps.forEach(s => { stepCounts[s] = 0; }); playProgress.forEach(p => { if (p.current_step in stepCounts) { stepCounts[p.current_step]++; } else { stepCounts[p.current_step] = (stepCounts[p.current_step] ?? 0) + 1; } }); // District breakdown const districtMap: Record = {}; playProgress.forEach(p => { const acct = filtered.accounts.find(a => a.Account_Name === p.Account_Name); const district = acct?.District_Name ?? 'Unknown'; if (!districtMap[district]) districtMap[district] = { assigned: 0, completed: 0, closedWon: 0 }; districtMap[district].assigned++; if (p.status === 'completed') districtMap[district].completed++; }); playPipeline.filter(d => d.Stage === '06-Closed Won').forEach(d => { if (!districtMap[d.District_Name]) districtMap[d.District_Name] = { assigned: 0, completed: 0, closedWon: 0 }; districtMap[d.District_Name].closedWon += (d.Closed_Amount_USD ?? d.Amount_USD); }); return { play, assigned, active, completed, pipelineGenerated, closedWon, conversionRate, avgDays, stepCounts, steps, districtMap, }; }, [playbooks, progress, filtered]); const metrics = useMemo(() => selectedPlayId ? getPlayMetrics(selectedPlayId) : null, [selectedPlayId, getPlayMetrics]); const compareMetrics = useMemo(() => comparePlayId ? getPlayMetrics(comparePlayId) : null, [comparePlayId, getPlayMetrics]); // Funnel chart data const funnelData = useMemo(() => { if (!metrics) return []; return [ { name: 'Assigned', value: metrics.assigned, fill: CHART_COLORS.navy }, { name: 'Active', value: metrics.active, fill: CHART_COLORS.azure }, { name: 'Completed', value: metrics.completed, fill: CHART_COLORS.aqua }, { name: 'Pipeline ($)', value: Math.round(metrics.pipelineGenerated / 1000), fill: CHART_COLORS.lightBlue }, { name: 'Closed Won ($)', value: Math.round(metrics.closedWon / 1000), fill: CHART_COLORS.green }, ]; }, [metrics]); // Step drop-off chart const stepDropoffData = useMemo(() => { if (!metrics) return []; const ordered = metrics.steps.length > 0 ? metrics.steps : Object.keys(metrics.stepCounts); return ordered.map((step, i) => ({ step: step.length > 20 ? step.slice(0, 18) + '...' : step, fullStep: step, count: metrics.stepCounts[step] ?? 0, fill: CHART_PALETTE[i % CHART_PALETTE.length], })); }, [metrics]); if (loading) { return ( <>
Loading playbook data...
); } if (playbooks.length === 0) { return ( <>

No Playbooks Found

Create playbooks and assign them to accounts to start tracking effectiveness and conversion funnels.

); } return ( <>
{/* Playbook selector and compare */}
{/* Playbook description */} {selectedPlaybook?.description && (

{selectedPlaybook.description}

)} {/* Scorecards */} {metrics && (
= 20 ? 'green' : metrics.conversionRate >= 10 ? 'amber' : 'red'} />
)} {/* Side-by-side comparison scorecards */} {compareMetrics && (
= 20 ? 'green' : compareMetrics.conversionRate >= 10 ? 'amber' : 'red'} />
)} {/* Funnel chart + Time in play */}
formatNumber(value as number)} /> {funnelData.map((entry, i) => ( ))}
{metrics ? Math.round(metrics.avgDays) : 0}
avg days
{metrics && metrics.completed > 0 && (
across {metrics.completed} completed plays
)} {compareMetrics && compareMetrics.completed > 0 && (
{Math.round(compareMetrics.avgDays)}
{compareMetrics.play.play_name}
)}
{/* Step drop-off analysis */} {stepDropoffData.length > 0 ? ( payload?.[0]?.payload?.fullStep ?? ''} formatter={(value) => `${formatNumber(value as number)} Accounts`} /> {stepDropoffData.map((entry, i) => ( ))} ) : (
No step data available
)}
{/* District comparison table */}
{metrics && Object.entries(metrics.districtMap) .sort(([, a], [, b]) => b.closedWon - a.closedWon) .map(([district, data]) => { const completionRate = data.assigned > 0 ? (data.completed / data.assigned) * 100 : 0; return ( ); })} {metrics && Object.keys(metrics.districtMap).length === 0 && ( )}
District Assigned Completed Completion % Closed Won
{DISTRICT_SHORT[district] ?? district} {data.assigned} {data.completed} = 50 ? 'text-success' : completionRate >= 25 ? 'text-warning' : 'text-danger'}> {formatPercent(completionRate, 0)} {formatCurrency(data.closedWon, true)}
No district data available
); }