Files
LynkedUpPro_CRM/src/pages/LeaderboardPage.jsx
T
Satyam bc4e25f132 feat: comprehensive mobile optimization, role-based AI chatbot, and multi-role dashboard enhancements
- Rewrote AI chatbot context system with deep role-aware data injection for all 7 roles (Owner, Admin, Field Agent, Contractor, Subcontractor, Vendor, Customer) plus guest fallback
- Added mobile-responsive bottom-sheet modals across all roles (ChangeOrderDrawer, InvoiceDetailModal, FinancialSummaryModal, VendorFinancialSummaryModal, FinancialDetailsModal)
- Converted Owner Project List and Vendor Orders tables to mobile card views with touch-friendly layouts
- Optimized Vendor Management and People Directory with mobile list/detail toggle and back navigation
- Fixed Document Control mobile view: horizontally scrollable filter tabs, proper overflow chain for document visibility
- Added responsive padding, text scaling, and flex-wrap across StatCard, TaskDetailsModal, OwnerSnapshot, OwnerProjectDetail, VendorDashboard, and DocumentManagement
- Expanded mock data store with richer vendor invoices, orders, documents, and compliance records
- Enhanced Owner Snapshot with financial KPI cards, urgent items panel, and Action Center modal
- Built Contractor Dashboard with task management, financial summary, and performance metrics
- Built Subcontractor Dashboard with clock-in tracking, task assignments, and invoice management
- Enhanced Vendor Dashboard with earnings summary, active orders, compliance status, and performance rating
- Added icon-only tab navigation on mobile for OwnerProjectDetail
- Extended attribution signatures across all platform pages
2026-02-18 12:34:55 +05:30

230 lines
13 KiB
React

import React, { useState, useMemo } from 'react';
import { useMockStore } from '../data/mockStore';
import { Trophy, TrendingUp, DollarSign, Target, Calendar, Medal, ArrowUpRight, Crown } from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard';
import AnimatedNumber from '../components/AnimatedNumber';
const LeaderboardPage = () => {
const { users, salesHistory } = useMockStore();
const [timeframe, setTimeframe] = useState('month'); // 'week', 'month', 'custom'
const [metric, setMetric] = useState('revenue'); // 'revenue', 'volume', 'winRate'
// Filter agents
const agents = useMemo(() => users.filter(u => u.role === 'FIELD_AGENT'), [users]);
// Calculate metrics based on timeframe
const leaderboardData = useMemo(() => {
const now = new Date();
const startOfWeek = new Date(now.setDate(now.getDate() - now.getDay()));
startOfWeek.setHours(0, 0, 0, 0);
const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
return agents.map(agent => {
const agentTxals = salesHistory.filter(tx => {
const txDate = new Date(tx.date);
const isAgent = tx.agentId === agent.id;
if (!isAgent) return false;
if (timeframe === 'week') return txDate >= startOfWeek;
if (timeframe === 'month') return txDate >= startOfMonth;
return true; // All time / Custom
});
// Calculate Metrics
const revenue = agentTxals
.filter(tx => tx.status === 'closed_won')
.reduce((sum, tx) => sum + tx.amount, 0);
const volume = agentTxals
.filter(tx => tx.status === 'closed_won')
.length;
const won = agentTxals.filter(tx => tx.status === 'closed_won').length;
const lost = agentTxals.filter(tx => tx.status === 'closed_lost').length;
const totalDeals = won + lost;
const winRate = totalDeals > 0 ? (won / totalDeals) * 100 : 0;
return {
...agent,
revenue,
volume,
winRate,
totalDeals
};
}).sort((a, b) => b[metric] - a[metric]);
}, [agents, salesHistory, timeframe, metric]);
const formatValue = (val, type) => {
if (type === 'revenue') return `$${val.toLocaleString()}`;
if (type === 'volume') return `${val} Deals`;
if (type === 'winRate') return `${val.toFixed(1)}%`;
return val;
};
const TopPodium = ({ data }) => {
if (data.length < 3) return null;
const [first, second, third] = data;
const PodiumStep = ({ agent, rank, height, color, glow }) => (
<div className="flex flex-col items-center z-10 mx-2 md:mx-4">
<div className="mb-4 text-center">
<div className="relative inline-block">
<div className={`w-16 h-16 md:w-20 md:h-20 rounded-full border-4 ${color} flex items-center justify-center bg-zinc-900/5 dark:bg-zinc-800 shadow-xl overflow-hidden mb-2 relative z-10`}>
{/* Placeholder Avatar */}
<span className="text-xl font-bold text-zinc-700 dark:text-zinc-300">
{agent.name.split(' ').map(n => n[0]).join('')}
</span>
</div>
{rank === 1 && <Crown size={24} className="absolute -top-4 -right-2 text-yellow-400 fill-yellow-400 animate-bounce" />}
<div className={`absolute inset-0 rounded-full blur-xl opacity-40 ${glow}`}></div>
</div>
<h3 className="font-bold text-sm md:text-base text-zinc-900 dark:text-white truncate max-w-[100px]">{agent.name}</h3>
<p className={`font-mono font-bold text-sm md:text-lg ${color.replace('border-', 'text-')}`}>
{metric === 'revenue' && <AnimatedNumber value={agent[metric]} prefix="$" useLocaleString duration={1.5} delay={rank * 0.2} />}
{metric === 'volume' && <><AnimatedNumber value={agent[metric]} duration={1.5} delay={rank * 0.2} /> Deals</>}
{metric === 'winRate' && <><AnimatedNumber value={agent[metric]} decimals={1} duration={1.5} delay={rank * 0.2} />%</>}
</p>
</div>
{/* The Step Block */}
<div className={`w-24 md:w-32 ${height} rounded-t-lg bg-gradient-to-b from-white/80 to-white/20 dark:from-white/10 dark:to-white/5 border-t border-x ${color.replace('border-', 'border-')} backdrop-blur-md relative overflow-hidden group transition-all duration-500 hover:brightness-110`}>
<div className="absolute inset-x-0 top-0 h-[1px] bg-white/50"></div>
<div className="w-full h-full flex items-end justify-center pb-4 text-4xl font-black text-black/5 dark:text-white/5 select-none">
{rank}
</div>
</div>
</div>
);
return (
<div className="flex items-end justify-center mb-12 pt-8">
<PodiumStep agent={second} rank={2} height="h-32" color="border-zinc-300" glow="bg-zinc-400" />
<PodiumStep agent={first} rank={1} height="h-44" color="border-yellow-400" glow="bg-yellow-500" />
<PodiumStep agent={third} rank={3} height="h-24" color="border-orange-400" glow="bg-orange-500" />
</div>
);
};
return (
<div className="min-h-full p-8 max-w-7xl mx-auto space-y-8 pb-20">
{/* Header */}
<header className="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div>
<h1 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-zinc-900 to-zinc-600 dark:from-white dark:to-white/60 mb-2 tracking-tight">Sales Leaderboard</h1>
<p className="text-zinc-500 dark:text-zinc-400 font-light">Real-time performance rankings for the sales team.</p>
</div>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Timeframe */}
<div className="bg-zinc-100 dark:bg-zinc-900/50 p-1 rounded-xl flex border border-zinc-200 dark:border-white/5">
{['week', 'month'].map(t => (
<button
key={t}
onClick={() => setTimeframe(t)}
className={`px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${timeframe === t
? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
This {t}
</button>
))}
</div>
{/* Metric */}
<div className="bg-zinc-100 dark:bg-zinc-900/50 p-1 rounded-xl flex border border-zinc-200 dark:border-white/5">
{[
{ id: 'revenue', icon: DollarSign, label: 'Revenue' },
{ id: 'volume', icon: Target, label: 'Volume' },
{ id: 'winRate', icon: TrendingUp, label: 'Win Rate' }
].map(m => (
<button
key={m.id}
onClick={() => setMetric(m.id)}
className={`flex items-center space-x-2 px-4 py-1.5 rounded-lg text-xs font-bold uppercase tracking-wider transition-all ${metric === m.id
? 'bg-white dark:bg-zinc-800 text-blue-600 dark:text-blue-300 shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white'}`}
>
<m.icon size={12} />
<span>{m.label}</span>
</button>
))}
</div>
</div>
</header>
{/* Podium Section */}
<div className="relative">
{/* Background Glow */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[300px] bg-blue-500/10 dark:bg-blue-900/20 blur-[100px] rounded-full pointer-events-none" />
<TopPodium data={leaderboardData} />
</div>
{/* Detailed List */}
<SpotlightCard className="overflow-hidden">
<div className="p-6">
<h3 className="text-lg font-bold text-zinc-900 dark:text-white mb-4 flex items-center">
<Trophy size={18} className="mr-2 text-zinc-400" />
Full Rankings
</h3>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="text-xs font-bold text-zinc-400 uppercase tracking-wider border-b border-zinc-200 dark:border-white/5">
<th className="pb-3 pl-4">Rank</th>
<th className="pb-3">Agent</th>
<th className="pb-3 text-right">Revenue</th>
<th className="pb-3 text-right">Volume</th>
<th className="pb-3 text-right">Win Rate</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100 dark:divide-white/5">
{leaderboardData.map((agent, index) => (
<tr key={agent.id} className="group hover:bg-zinc-50 dark:hover:bg-white/5 transition-colors">
<td className="py-4 pl-4">
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm ${index === 0 ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-300' :
index === 1 ? 'bg-zinc-100 text-zinc-700 dark:bg-white/10 dark:text-zinc-300' :
index === 2 ? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300' :
'text-zinc-500'
}`}>
{index + 1}
</div>
</td>
<td className="py-4">
<div className="flex items-center">
<div className="w-8 h-8 rounded-full bg-zinc-200 dark:bg-zinc-700 flex items-center justify-center text-xs font-bold text-zinc-500 dark:text-zinc-400 mr-3">
{agent.name.charAt(0)}
</div>
<div>
<div className="font-semibold text-zinc-900 dark:text-white">{agent.name}</div>
<div className="text-xs text-zinc-500">{agent.email}</div>
</div>
</div>
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'revenue' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
<AnimatedNumber value={agent.revenue} prefix="$" useLocaleString duration={1.2} delay={index * 0.05} />
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'volume' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
<AnimatedNumber value={agent.volume} duration={1.2} delay={index * 0.05} /> Deals
</td>
<td className={`py-4 text-right font-mono font-medium ${metric === 'winRate' ? 'text-blue-600 dark:text-blue-400 font-bold' : 'text-zinc-500 dark:text-zinc-400'}`}>
<AnimatedNumber value={agent.winRate} decimals={1} duration={1.2} delay={index * 0.05} />%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</SpotlightCard>
<span className="attribution-ghost">igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?</span>
</div>
);
};
export default LeaderboardPage;