import React, { useState, useMemo } from 'react'; import { motion } from 'framer-motion'; import { useMockStore } from '../data/mockStore'; import { Trophy, TrendingUp, DollarSign, Target, Crown, ShieldOff, Users, CheckCircle2, Clock, BarChart2, MapPin } from 'lucide-react'; import { SpotlightCard } from '../components/SpotlightCard'; import AnimatedNumber from '../components/AnimatedNumber'; import { usePermissions } from '../hooks/usePermissions'; // ── Rank badge helper ──────────────────────────────────────────────────────── const rankBadge = (i) => i === 0 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300' : i === 1 ? 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-300' : i === 2 ? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' : 'text-zinc-500'; // ── Podium tier styling (gold / silver / bronze) ─────────────────────────────── // Solid metal bars with dark, clearly-legible rank numerals. Height encodes rank: // 1st is tallest, then 2nd, then 3rd. const PODIUM_TIERS = { 1: { heightPx: 200, border: 'border-yellow-400', avatarRing: 'border-yellow-400', bar: 'bg-gradient-to-b from-yellow-300 to-yellow-500', numeral: 'text-yellow-900/80', text: 'text-yellow-600 dark:text-yellow-400', glow: 'bg-yellow-500', }, 2: { heightPx: 144, border: 'border-zinc-300', avatarRing: 'border-zinc-300', bar: 'bg-gradient-to-b from-zinc-200 to-zinc-400', numeral: 'text-zinc-700/80', text: 'text-zinc-500 dark:text-zinc-300', glow: 'bg-zinc-400', }, 3: { heightPx: 100, border: 'border-orange-400', avatarRing: 'border-orange-400', bar: 'bg-gradient-to-b from-orange-300 to-orange-500', numeral: 'text-orange-900/80', text: 'text-orange-600 dark:text-orange-400', glow: 'bg-orange-500', }, }; // ── Podium step (single column) ─────────────────────────────────────────────── // Bars grow from a shared baseline at a constant speed, so the shortest (bronze) // finishes first, then silver, and the tallest (gold) lands last. const PodiumStep = ({ agent, rank, metricKey, numberFormat = {} }) => { const tier = PODIUM_TIERS[rank]; return (
{agent.name.split(' ').map(n => n[0]).join('')}
{rank === 1 && }

{agent.name}

{rank}
); }; // ── Podium (shared) ────────────────────────────────────────────────────────── const TopPodium = ({ data, metricKey, numberFormat = {} }) => { if (data.length < 3) return null; const [first, second, third] = data; return ( // Fixed min-height reserves the full podium space so the shared baseline // never shifts while the bars animate — they grow upward from the floor.
); }; // ── Tab pill ───────────────────────────────────────────────────────────────── const TabBtn = ({ active, onClick, icon: Icon, label, accent }) => ( ); // ── Filter pill row ─────────────────────────────────────────────────────────── const FilterPills = ({ options, value, onChange }) => (
{options.map(o => ( ))}
); // ════════════════════════════════════════════════════════════════════════════ // MAIN PAGE // ════════════════════════════════════════════════════════════════════════════ const LeaderboardPage = () => { const { users, salesHistory, canvasserHistory } = useMockStore(); const { can } = usePermissions(); // Tab: 'sales' | 'canvasser' const [tab, setTab] = useState('sales'); // ── Sales filters ──────────────────────────────────────────────────────── const [timeframe, setTimeframe] = useState('all'); const [metric, setMetric] = useState('revenue'); // ── Canvasser filters ──────────────────────────────────────────────────── const [cvTimeframe, setCvTimeframe] = useState('all'); const [cvMetric, setCvMetric] = useState('verified'); // ── Agents ─────────────────────────────────────────────────────────────── const agents = useMemo(() => users.filter(u => u.role === 'FIELD_AGENT'), [users]); // ── Date windows ───────────────────────────────────────────────────────── // Returns null for 'all' (no date filter), or a Date boundary for week/month. const getWindow = (tf) => { if (tf === 'all') return null; const now = new Date(); if (tf === 'week') { const d = new Date(now); d.setDate(d.getDate() - d.getDay()); d.setHours(0, 0, 0, 0); return d; } return new Date(now.getFullYear(), now.getMonth(), 1); }; // ── Sales leaderboard data ─────────────────────────────────────────────── const salesData = useMemo(() => { const since = getWindow(timeframe); return agents.map(agent => { const txs = salesHistory.filter(tx => { if (tx.agentId !== agent.id) return false; if (since === null) return true; // 'all time' — no date filter return new Date(tx.date) >= since; }); const revenue = txs.filter(t => t.status === 'closed_won').reduce((s, t) => s + t.amount, 0); const volume = txs.filter(t => t.status === 'closed_won').length; const won = txs.filter(t => t.status === 'closed_won').length; const lost = txs.filter(t => t.status === 'closed_lost').length; const winRate = (won + lost) > 0 ? (won / (won + lost)) * 100 : 0; return { ...agent, revenue, volume, winRate, totalDeals: won + lost }; }).sort((a, b) => b[metric] - a[metric]); }, [agents, salesHistory, timeframe, metric]); // ── Canvasser leaderboard data ─────────────────────────────────────────── // Single source of truth: canvasserHistory records, filtered by date window. // verified = records with status 'verified' OR 'converted' (converted is also verified) // unverified = records with status 'unverified' (pending review) // converted = records with status 'converted' (became paying client) // total = all door-knock records in the window // convRate = converted / verified — share of verified leads that closed const canvasserData = useMemo(() => { const since = getWindow(cvTimeframe); return agents.map(agent => { const records = (canvasserHistory || []).filter(r => { if (r.agentId !== agent.id) return false; if (!since) return true; return new Date(r.date) >= since; }); const verified = records.filter(r => r.status === 'verified' || r.status === 'converted').length; const unverified = records.filter(r => r.status === 'unverified').length; const converted = records.filter(r => r.status === 'converted').length; const total = records.length; const convRate = verified > 0 ? (converted / verified) * 100 : 0; return { ...agent, verified, unverified, converted, total, convRate }; }).sort((a, b) => b[cvMetric] - a[cvMetric]); }, [agents, canvasserHistory, cvTimeframe, cvMetric]); // ── Permission guard ───────────────────────────────────────────────────── if (!can('leaderboard', 'view')) { return (

Access Restricted

You don't have permission to view the Leaderboard. Contact your org admin for access.

); } // ── Render ─────────────────────────────────────────────────────────────── return (
{/* ── Page header ── */}

{tab === 'sales' ? 'Sales Leaderboard' : 'Canvasser Leaderboard'}

{tab === 'sales' ? 'Real-time performance rankings for the sales team.' : 'Verified door-knock lead rankings for canvassers.'}

{/* ── Tab switcher ── */}
setTab('sales')} icon={DollarSign} label="Sales" accent="bg-white dark:bg-zinc-800 border-zinc-200 dark:border-white/10 text-blue-600 dark:text-blue-400 shadow-sm" /> setTab('canvasser')} icon={MapPin} label="Canvasser" accent="bg-white dark:bg-zinc-800 border-zinc-200 dark:border-white/10 text-emerald-600 dark:text-emerald-400 shadow-sm" />
{/* ════════════════════════════════════════════════ SALES TAB ════════════════════════════════════════════════ */} {tab === 'sales' && ( <> {/* Controls */}
{/* Podium */}
{/* Full table */}

Full Rankings

{salesData.map((agent, i) => ( ))}
Rank Agent Revenue Volume Win Rate
{i + 1}
{agent.name.charAt(0)}
{agent.name}
{agent.email}
Deals %
)} {/* ════════════════════════════════════════════════ CANVASSER TAB ════════════════════════════════════════════════ */} {tab === 'canvasser' && ( <> {/* Controls */}
{/* Podium */}
{/* Stat summary cards */}
{[ { label: 'Total Verified', value: canvasserData.reduce((s, a) => s + a.verified, 0), color: 'text-emerald-500', icon: CheckCircle2 }, { label: 'Unverified (Pending)',value: canvasserData.reduce((s, a) => s + a.unverified, 0), color: 'text-amber-500', icon: Clock }, { label: 'Converted to Client', value: canvasserData.reduce((s, a) => s + a.converted, 0), color: 'text-blue-500', icon: TrendingUp }, { label: 'Total Door Knocks', value: canvasserData.reduce((s, a) => s + a.total, 0), color: 'text-violet-500', icon: MapPin }, ].map(c => (
{c.label}

))}
{/* Full table */}

Canvasser Rankings

{canvasserData.map((agent, i) => ( ))}
Rank Canvasser Verified Unverified Converted Conv. Rate
{i + 1}
{agent.name.charAt(0)}
{agent.name}
{agent.email}
%
)} igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; export default LeaderboardPage;