diff --git a/src/components/AnimatedNumber.jsx b/src/components/AnimatedNumber.jsx index e585716..50754c0 100644 --- a/src/components/AnimatedNumber.jsx +++ b/src/components/AnimatedNumber.jsx @@ -106,7 +106,7 @@ const AnimatedNumber = ({ }; }, [value, duration, prefix, suffix, decimals, useLocaleString, delay]); - return 0; + return {value ?? 0}; }; export default AnimatedNumber; diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index 0bedd8a..6c79776 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -736,33 +736,100 @@ const MOCK_USERS = [ // Amounts reflect residential storm-damage roofing jobs ($12K–$42K per deal). const MOCK_SALES_HISTORY = [ // Frank Agent (High Revenue, Low Volume - "The Sniper") — fewer deals, bigger jobs - { id: 'tx_1', agentId: 'e1', date: '2026-03-19', amount: 38500, status: 'closed_won' }, // this week - { id: 'tx_2', agentId: 'e1', date: '2026-03-12', amount: 42000, status: 'closed_won' }, - { id: 'tx_3', agentId: 'e1', date: '2026-03-06', amount: 31800, status: 'closed_won' }, - { id: 'tx_4', agentId: 'e1', date: '2026-03-03', amount: 0, status: 'closed_lost' }, + { id: 'tx_1', agentId: 'e1', date: '2026-05-14', amount: 38500, status: 'closed_won' }, // this week + { id: 'tx_2', agentId: 'e1', date: '2026-05-12', amount: 42000, status: 'closed_won' }, // this week + { id: 'tx_3', agentId: 'e1', date: '2026-05-06', amount: 31800, status: 'closed_won' }, + { id: 'tx_4', agentId: 'e1', date: '2026-05-03', amount: 0, status: 'closed_lost' }, // Fiona Field (High Volume, Mid Revenue - "The Grinder") — more deals, consistent value - { id: 'tx_5', agentId: 'e2', date: '2026-03-17', amount: 18200, status: 'closed_won' }, // this week - { id: 'tx_6', agentId: 'e2', date: '2026-03-16', amount: 16800, status: 'closed_won' }, // this week - { id: 'tx_7', agentId: 'e2', date: '2026-03-11', amount: 21400, status: 'closed_won' }, - { id: 'tx_8', agentId: 'e2', date: '2026-03-08', amount: 19600, status: 'closed_won' }, - { id: 'tx_9', agentId: 'e2', date: '2026-03-05', amount: 17300, status: 'closed_won' }, - { id: 'tx_10', agentId: 'e2', date: '2026-03-02', amount: 0, status: 'closed_lost' }, - { id: 'tx_11', agentId: 'e2', date: '2026-03-01', amount: 0, status: 'closed_lost' }, + { id: 'tx_5', agentId: 'e2', date: '2026-05-13', amount: 18200, status: 'closed_won' }, // this week + { id: 'tx_6', agentId: 'e2', date: '2026-05-11', amount: 16800, status: 'closed_won' }, // this week + { id: 'tx_7', agentId: 'e2', date: '2026-05-08', amount: 21400, status: 'closed_won' }, + { id: 'tx_8', agentId: 'e2', date: '2026-05-06', amount: 19600, status: 'closed_won' }, + { id: 'tx_9', agentId: 'e2', date: '2026-05-05', amount: 17300, status: 'closed_won' }, + { id: 'tx_10', agentId: 'e2', date: '2026-05-02', amount: 0, status: 'closed_lost' }, + { id: 'tx_11', agentId: 'e2', date: '2026-05-01', amount: 0, status: 'closed_lost' }, // Fred Flyer (New/Low Performance) — one close this month, struggling - { id: 'tx_12', agentId: 'e3', date: '2026-03-18', amount: 12400, status: 'closed_won' }, // this week - { id: 'tx_13', agentId: 'e3', date: '2026-03-09', amount: 0, status: 'closed_lost' }, - { id: 'tx_14', agentId: 'e3', date: '2026-03-04', amount: 0, status: 'closed_lost' }, + { id: 'tx_12', agentId: 'e3', date: '2026-05-13', amount: 12400, status: 'closed_won' }, // this week + { id: 'tx_13', agentId: 'e3', date: '2026-05-07', amount: 0, status: 'closed_lost' }, + { id: 'tx_14', agentId: 'e3', date: '2026-05-04', amount: 0, status: 'closed_lost' }, // Felicity Fast (Consistent) — reliable mid-tier producer - { id: 'tx_15', agentId: 'e4', date: '2026-03-15', amount: 24800, status: 'closed_won' }, // this week - { id: 'tx_16', agentId: 'e4', date: '2026-03-10', amount: 23200, status: 'closed_won' }, - { id: 'tx_17', agentId: 'e4', date: '2026-03-07', amount: 26500, status: 'closed_won' }, + { id: 'tx_15', agentId: 'e4', date: '2026-05-12', amount: 24800, status: 'closed_won' }, // this week + { id: 'tx_16', agentId: 'e4', date: '2026-05-08', amount: 23200, status: 'closed_won' }, + { id: 'tx_17', agentId: 'e4', date: '2026-05-05', amount: 26500, status: 'closed_won' }, // Felix Fixer (Closer - High Win Rate) — doesn't lose, picks his battles - { id: 'tx_18', agentId: 'e5', date: '2026-03-20', amount: 29500, status: 'closed_won' }, // this week - { id: 'tx_19', agentId: 'e5', date: '2026-03-11', amount: 34800, status: 'closed_won' }, + { id: 'tx_18', agentId: 'e5', date: '2026-05-14', amount: 29500, status: 'closed_won' }, // this week + { id: 'tx_19', agentId: 'e5', date: '2026-05-08', amount: 34800, status: 'closed_won' }, +]; + +// --- MOCK CANVASSER DOOR-KNOCK HISTORY --- +// Each record = one door-knock session / lead attempt by a canvasser. +// status: 'verified' | 'unverified' | 'converted' +// verified = lead confirmed by team lead / admin +// unverified = logged by canvasser, pending review +// converted = verified lead that became a paying client +// Dates in May 2026 — days 11-14 fall in current week, all in current month. +const MOCK_CANVASSER_HISTORY = [ + // Frank Agent (e1) — solid verifier, good conversion + { id: 'ck_001', agentId: 'e1', date: '2026-05-14', status: 'verified', address: '2612 Dunwick Dr' }, + { id: 'ck_002', agentId: 'e1', date: '2026-05-14', status: 'converted', address: '2608 Dunwick Dr' }, + { id: 'ck_003', agentId: 'e1', date: '2026-05-13', status: 'verified', address: '2604 Dunwick Dr' }, + { id: 'ck_004', agentId: 'e1', date: '2026-05-13', status: 'unverified', address: '2600 Dunwick Dr' }, + { id: 'ck_005', agentId: 'e1', date: '2026-05-12', status: 'converted', address: '2516 Dunwick Dr' }, + { id: 'ck_006', agentId: 'e1', date: '2026-05-08', status: 'verified', address: '2637 Rothland Ln' }, + { id: 'ck_007', agentId: 'e1', date: '2026-05-08', status: 'unverified', address: '2633 Rothland Ln' }, + { id: 'ck_008', agentId: 'e1', date: '2026-05-06', status: 'verified', address: '2629 Rothland Ln' }, + { id: 'ck_009', agentId: 'e1', date: '2026-05-04', status: 'verified', address: '6613 Phoenix Pl' }, + { id: 'ck_010', agentId: 'e1', date: '2026-05-02', status: 'converted', address: '6609 Phoenix Pl' }, + + // Fiona Field (e2) — high volume, moderate verification rate + { id: 'ck_011', agentId: 'e2', date: '2026-05-14', status: 'verified', address: '6605 Phoenix Pl' }, + { id: 'ck_012', agentId: 'e2', date: '2026-05-14', status: 'unverified', address: '6601 Phoenix Pl' }, + { id: 'ck_013', agentId: 'e2', date: '2026-05-13', status: 'converted', address: '6617 Phoenix Pl' }, + { id: 'ck_014', agentId: 'e2', date: '2026-05-13', status: 'verified', address: '3913 Arizona Pl' }, + { id: 'ck_015', agentId: 'e2', date: '2026-05-11', status: 'unverified', address: '3917 Arizona Pl' }, + { id: 'ck_016', agentId: 'e2', date: '2026-05-08', status: 'verified', address: '3921 Arizona Pl' }, + { id: 'ck_017', agentId: 'e2', date: '2026-05-08', status: 'unverified', address: '3920 Arizona Pl' }, + { id: 'ck_018', agentId: 'e2', date: '2026-05-06', status: 'converted', address: '3916 Arizona Pl' }, + { id: 'ck_019', agentId: 'e2', date: '2026-05-05', status: 'verified', address: '3912 Arizona Pl' }, + { id: 'ck_020', agentId: 'e2', date: '2026-05-04', status: 'unverified', address: '3908 Arizona Pl' }, + { id: 'ck_021', agentId: 'e2', date: '2026-05-03', status: 'verified', address: '3904 Arizona Pl' }, + { id: 'ck_022', agentId: 'e2', date: '2026-05-01', status: 'unverified', address: '3900 Arizona Pl' }, + + // Fred Flyer (e3) — low volume, mostly unverified (new canvasser) + { id: 'ck_023', agentId: 'e3', date: '2026-05-14', status: 'unverified', address: '3917 Sailmaker Ln' }, + { id: 'ck_024', agentId: 'e3', date: '2026-05-12', status: 'verified', address: '3913 Sailmaker Ln' }, + { id: 'ck_025', agentId: 'e3', date: '2026-05-07', status: 'unverified', address: '3909 Sailmaker Ln' }, + { id: 'ck_026', agentId: 'e3', date: '2026-05-05', status: 'unverified', address: '3905 Sailmaker Ln' }, + { id: 'ck_027', agentId: 'e3', date: '2026-05-03', status: 'converted', address: '3901 Sailmaker Ln' }, + + // Felicity Fast (e4) — top performer, best verified count + conversion + { id: 'ck_028', agentId: 'e4', date: '2026-05-14', status: 'converted', address: '2612 Dunwick Dr' }, + { id: 'ck_029', agentId: 'e4', date: '2026-05-14', status: 'verified', address: '2608 Dunwick Dr' }, + { id: 'ck_030', agentId: 'e4', date: '2026-05-13', status: 'converted', address: '2604 Dunwick Dr' }, + { id: 'ck_031', agentId: 'e4', date: '2026-05-12', status: 'verified', address: '2600 Dunwick Dr' }, + { id: 'ck_032', agentId: 'e4', date: '2026-05-11', status: 'verified', address: '6613 Phoenix Pl' }, + { id: 'ck_033', agentId: 'e4', date: '2026-05-08', status: 'converted', address: '6609 Phoenix Pl' }, + { id: 'ck_034', agentId: 'e4', date: '2026-05-07', status: 'verified', address: '6605 Phoenix Pl' }, + { id: 'ck_035', agentId: 'e4', date: '2026-05-06', status: 'unverified', address: '6601 Phoenix Pl' }, + { id: 'ck_036', agentId: 'e4', date: '2026-05-05', status: 'converted', address: '6617 Phoenix Pl' }, + { id: 'ck_037', agentId: 'e4', date: '2026-05-04', status: 'verified', address: '3913 Arizona Pl' }, + { id: 'ck_038', agentId: 'e4', date: '2026-05-03', status: 'verified', address: '3917 Arizona Pl' }, + { id: 'ck_039', agentId: 'e4', date: '2026-05-02', status: 'unverified', address: '3921 Arizona Pl' }, + { id: 'ck_040', agentId: 'e4', date: '2026-05-01', status: 'converted', address: '3920 Arizona Pl' }, + { id: 'ck_041', agentId: 'e4', date: '2026-05-01', status: 'verified', address: '3916 Arizona Pl' }, + + // Felix Fixer (e5) — selective, fewer knocks, decent conversion rate + { id: 'ck_042', agentId: 'e5', date: '2026-05-14', status: 'converted', address: '3912 Arizona Pl' }, + { id: 'ck_043', agentId: 'e5', date: '2026-05-13', status: 'verified', address: '3908 Arizona Pl' }, + { id: 'ck_044', agentId: 'e5', date: '2026-05-11', status: 'unverified', address: '3904 Arizona Pl' }, + { id: 'ck_045', agentId: 'e5', date: '2026-05-07', status: 'converted', address: '3900 Arizona Pl' }, + { id: 'ck_046', agentId: 'e5', date: '2026-05-05', status: 'verified', address: '3917 Sailmaker Ln' }, + { id: 'ck_047', agentId: 'e5', date: '2026-05-03', status: 'unverified', address: '3913 Sailmaker Ln' }, + { id: 'ck_048', agentId: 'e5', date: '2026-05-01', status: 'verified', address: '3909 Sailmaker Ln' }, ]; // Helper to generate meetings @@ -5150,6 +5217,7 @@ export const MockStoreProvider = ({ children }) => { users, meetings, salesHistory, // Exported for Leaderboard + canvasserHistory: MOCK_CANVASSER_HISTORY, // Exported for Canvasser Leaderboard owners, personnel, vendors, diff --git a/src/pages/LeaderboardPage.jsx b/src/pages/LeaderboardPage.jsx index 7473a31..3bf8dd9 100644 --- a/src/pages/LeaderboardPage.jsx +++ b/src/pages/LeaderboardPage.jsx @@ -1,115 +1,181 @@ import React, { useState, useMemo } from 'react'; import { useMockStore } from '../data/mockStore'; -import { Trophy, TrendingUp, DollarSign, Target, Calendar, Medal, ArrowUpRight, Crown, ShieldOff } from 'lucide-react'; +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'; -const LeaderboardPage = () => { - const { users, salesHistory } = useMockStore(); - const { can } = usePermissions(); - const [timeframe, setTimeframe] = useState('month'); // 'week', 'month', 'custom' - const [metric, setMetric] = useState('revenue'); // 'revenue', 'volume', 'winRate' +// ── 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'; - // Filter agents +// ── Podium (shared) ────────────────────────────────────────────────────────── +const TopPodium = ({ data, metricKey, formatVal, accentColor = 'border-yellow-400' }) => { + if (data.length < 3) return null; + const [first, second, third] = data; + const Step = ({ agent, rank, height, color, glow }) => ( +
+ {formatVal(agent[metricKey])} +
+
- {metric === 'revenue' &&
Real-time performance rankings for the sales team.
++ {tab === 'sales' + ? 'Real-time performance rankings for the sales team.' + : 'Verified door-knock lead rankings for canvassers.'} +
| Rank | -Agent | -Revenue | -Volume | -Win Rate | -
|---|---|---|---|---|
|
-
- {index + 1}
-
- |
-
-
-
-
- {agent.name.charAt(0)}
-
-
-
- {agent.name}
- {agent.email}
- |
-
- |
-
- |
-
- |
-
| Rank | +Agent | +Revenue | +Volume | +Win Rate | +
|---|---|---|---|---|
|
+ {i + 1}
+ |
+
+
+
+
+ {agent.name.charAt(0)}
+
+
+
+ {agent.name}
+ {agent.email}
+ |
+
+ |
+
+ |
+
+ |
+
+
| Rank | +Canvasser | +Verified | +Unverified | +Converted | +Conv. Rate | +
|---|---|---|---|---|---|
|
+ {i + 1}
+ |
+
+
+
+
+ {agent.name.charAt(0)}
+
+
+
+ {agent.name}
+ {agent.email}
+ |
+
+ |
+
+ |
+
+ |
+
+ |
+