Add full leads pipeline: Lead/LeadRun types, 3 new DB tables (leads, leads_runs, leads_config), 12 CRUD functions including syncBriefsFromDisk and exportAccountsForLoop, 12 API actions for leads lifecycle management. New pages: /leads (filterable lead cards with expand/action), /leads/admin (run controls, config, run history). AccountLeads component in account detail view. New Leads scorecard on Executive Overview. Sidebar nav entry. Also fixes account detail activity list using raw.activities to show all activities regardless of date filter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
253 lines
13 KiB
TypeScript
253 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useState } from 'react';
|
|
|
|
function DashboardIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="4" rx="1" /><rect x="14" y="10" width="7" height="11" rx="1" /><rect x="3" y="13" width="7" height="8" rx="1" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ActivityIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function PipelineIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ImplementIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function PlaybookIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function GoalsIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ReportsIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function ExecIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" /><line x1="8" y1="21" x2="16" y2="21" /><line x1="12" y1="17" x2="12" y2="21" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function AnalyticsIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /><polyline points="7.5 4.21 12 6.81 16.5 4.21" /><polyline points="7.5 19.79 7.5 14.6 3 12" /><polyline points="21 12 16.5 14.6 16.5 19.79" /><polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function AccountsIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M23 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function LeadsIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
const navItems = [
|
|
{ href: '/', label: 'Executive Overview', icon: DashboardIcon, shortLabel: 'Overview' },
|
|
{ href: '/accounts', label: 'Accounts', icon: AccountsIcon, shortLabel: 'Accounts' },
|
|
{ href: '/leads', label: 'Leads', icon: LeadsIcon, shortLabel: 'Leads' },
|
|
{ href: '/activity', label: 'Activities', icon: ActivityIcon, shortLabel: 'Activities' },
|
|
{ href: '/pipeline', label: 'Opportunities', icon: PipelineIcon, shortLabel: 'Opps' },
|
|
{ href: '/implementation', label: 'Implementation', icon: ImplementIcon, shortLabel: 'Implement' },
|
|
{ href: '/goals', label: 'Goals & Targets', icon: GoalsIcon, shortLabel: 'Goals' },
|
|
{ href: '/playbooks', label: 'Playbooks', icon: PlaybookIcon, shortLabel: 'Plays' },
|
|
{ href: '/reports/status', label: 'Weekly Report', icon: ReportsIcon, shortLabel: 'Report' },
|
|
{ href: '/reports/executive', label: 'Exec Summary', icon: ExecIcon, shortLabel: 'Summary' },
|
|
];
|
|
|
|
const analyticsItems = [
|
|
{ href: '/analytics/pipeline-movement', label: 'Pipeline Movement' },
|
|
{ href: '/analytics/forecast', label: 'Forecast Workspace' },
|
|
{ href: '/analytics/forecast-accuracy', label: 'Forecast Accuracy' },
|
|
{ href: '/analytics/win-loss', label: 'Win/Loss Analysis' },
|
|
{ href: '/analytics/playbook-effectiveness', label: 'Playbook Effectiveness' },
|
|
];
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [analyticsOpen, setAnalyticsOpen] = useState(pathname.startsWith('/analytics'));
|
|
const isAnalyticsActive = pathname.startsWith('/analytics');
|
|
|
|
return (
|
|
<>
|
|
{/* Desktop sidebar */}
|
|
<aside className={`hidden lg:flex flex-col bg-sidebar-bg text-sidebar-text ${collapsed ? 'w-16' : 'w-56'} transition-all duration-200 min-h-screen`}>
|
|
<div className={`flex items-center ${collapsed ? 'justify-center px-2' : 'px-4'} h-16 border-b border-white/10`}>
|
|
{!collapsed && (
|
|
<Link href="/" className="flex items-center gap-2">
|
|
<img src="/logo.svg" alt="Logo" className="h-8 w-8" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
|
<div>
|
|
<div className="text-white font-bold text-sm tracking-wide">AgentMinder</div>
|
|
<div className="text-[10px] text-sidebar-text/60 tracking-wider uppercase">Command Center</div>
|
|
</div>
|
|
</Link>
|
|
)}
|
|
<button
|
|
onClick={() => setCollapsed(!collapsed)}
|
|
className={`${collapsed ? '' : 'ml-auto'} p-1 rounded hover:bg-white/10 transition`}
|
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
>
|
|
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
{collapsed ? <polyline points="9 18 15 12 9 6" /> : <polyline points="15 18 9 12 15 6" />}
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<nav className="flex-1 py-4 space-y-1 overflow-y-auto">
|
|
{navItems.map(item => {
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`flex items-center gap-3 ${collapsed ? 'justify-center px-2' : 'px-4'} py-2.5 mx-2 rounded-lg transition-all ${
|
|
isActive
|
|
? 'bg-sidebar-active/20 text-white'
|
|
: 'hover:bg-white/5 text-sidebar-text hover:text-white'
|
|
}`}
|
|
title={collapsed ? item.label : undefined}
|
|
>
|
|
<item.icon className={`w-5 h-5 flex-shrink-0 ${isActive ? 'text-sidebar-active' : ''}`} />
|
|
{!collapsed && <span className="text-sm font-medium">{item.label}</span>}
|
|
</Link>
|
|
);
|
|
})}
|
|
|
|
{/* Analytics section */}
|
|
{!collapsed ? (
|
|
<>
|
|
<button
|
|
onClick={() => setAnalyticsOpen(!analyticsOpen)}
|
|
className={`flex items-center gap-3 px-4 py-2.5 mx-2 rounded-lg transition-all w-full text-left ${
|
|
isAnalyticsActive ? 'bg-sidebar-active/20 text-white' : 'hover:bg-white/5 text-sidebar-text hover:text-white'
|
|
}`}
|
|
>
|
|
<AnalyticsIcon className={`w-5 h-5 flex-shrink-0 ${isAnalyticsActive ? 'text-sidebar-active' : ''}`} />
|
|
<span className="text-sm font-medium flex-1">Analytics</span>
|
|
<svg className={`w-3.5 h-3.5 transition-transform ${analyticsOpen ? 'rotate-90' : ''}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="9 18 15 12 9 6" /></svg>
|
|
</button>
|
|
{analyticsOpen && (
|
|
<div className="ml-8 mr-2 space-y-0.5">
|
|
{analyticsItems.map(item => {
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`block px-3 py-1.5 rounded-md text-xs font-medium transition-all ${
|
|
isActive ? 'bg-sidebar-active/20 text-white' : 'text-sidebar-text/70 hover:text-white hover:bg-white/5'
|
|
}`}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Link
|
|
href="/analytics/pipeline-movement"
|
|
className={`flex items-center justify-center px-2 py-2.5 mx-2 rounded-lg transition-all ${
|
|
isAnalyticsActive ? 'bg-sidebar-active/20 text-white' : 'hover:bg-white/5 text-sidebar-text hover:text-white'
|
|
}`}
|
|
title="Analytics"
|
|
>
|
|
<AnalyticsIcon className={`w-5 h-5 flex-shrink-0 ${isAnalyticsActive ? 'text-sidebar-active' : ''}`} />
|
|
</Link>
|
|
)}
|
|
</nav>
|
|
<div className={`${collapsed ? 'px-2' : 'px-4'} pb-4 space-y-3`}>
|
|
<Link
|
|
href="/admin"
|
|
className={`flex items-center gap-2 ${collapsed ? 'justify-center px-2' : 'px-3'} py-2 rounded-lg text-sidebar-text/60 hover:text-white hover:bg-white/5 transition text-xs`}
|
|
title={collapsed ? 'Data Admin' : undefined}
|
|
>
|
|
<svg className="w-4 h-4 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
|
</svg>
|
|
{!collapsed && <span>Data Admin</span>}
|
|
</Link>
|
|
<div className={`rounded-lg bg-white/5 ${collapsed ? 'p-2' : 'p-3'}`}>
|
|
{!collapsed && (
|
|
<div className="text-[10px] uppercase tracking-wider text-sidebar-text/50 mb-1">Territory</div>
|
|
)}
|
|
<div className={`text-xs text-white font-medium ${collapsed ? 'text-center' : ''}`}>
|
|
{collapsed ? 'SE' : 'SouthEast'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
{/* Mobile bottom nav */}
|
|
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-card-border z-50 flex">
|
|
{navItems.map(item => {
|
|
const isActive = pathname === item.href;
|
|
return (
|
|
<Link
|
|
key={item.href}
|
|
href={item.href}
|
|
className={`flex-1 flex flex-col items-center py-2 text-[10px] font-medium transition ${
|
|
isActive ? 'text-brand-navy' : 'text-muted'
|
|
}`}
|
|
>
|
|
<item.icon className={`w-5 h-5 mb-0.5 ${isActive ? 'text-brand-navy' : 'text-muted'}`} />
|
|
{item.shortLabel}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</>
|
|
);
|
|
}
|