feat(dashboard): show analytics+financial cards to owner; ground Top Sales leaderboard in real reps; fix en-US currency formatting
This commit is contained in:
@@ -1,18 +1,15 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { SpotlightCard } from '../SpotlightCard';
|
||||
import { Trophy, ChevronRight, User } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useMockStore } from '../../data/mockStore';
|
||||
import { repLeaderboard } from '../../data/selectors';
|
||||
|
||||
const MOCK_TOP_REPS = [
|
||||
{ id: 1, name: "Sarah Connor", sales: "$1.2M", deals: 42, avatar: null },
|
||||
{ id: 2, name: "Michael Ross", sales: "$980K", deals: 35, avatar: null },
|
||||
{ id: 3, name: "Jessica Pearson", sales: "$850K", deals: 28, avatar: null },
|
||||
{ id: 4, name: "Harvey Specter", sales: "$720K", deals: 24, avatar: null },
|
||||
{ id: 5, name: "Louis Litt", sales: "$640K", deals: 20, avatar: null },
|
||||
{ id: 6, name: "Donna Paulsen", sales: "$590K", deals: 18, avatar: null },
|
||||
];
|
||||
const fmtSales = (n) => n >= 1_000_000 ? `$${(n / 1_000_000).toFixed(1)}M` : `$${Math.round(n / 1000)}K`;
|
||||
|
||||
export const TopSalesLeaderboard = () => {
|
||||
const { projects, resolvePerson } = useMockStore();
|
||||
const reps = useMemo(() => repLeaderboard(projects, resolvePerson), [projects, resolvePerson]);
|
||||
return (
|
||||
<SpotlightCard className="h-full flex flex-col">
|
||||
<div className="p-6 flex-1 flex flex-col min-h-0">
|
||||
@@ -32,7 +29,7 @@ export const TopSalesLeaderboard = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 space-y-3 overflow-y-auto pr-1 custom-scrollbar">
|
||||
{MOCK_TOP_REPS.map((rep, index) => (
|
||||
{reps.map((rep, index) => (
|
||||
<Link
|
||||
to="/admin/leaderboard"
|
||||
key={rep.id}
|
||||
@@ -69,7 +66,7 @@ export const TopSalesLeaderboard = () => {
|
||||
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-black text-zinc-900 dark:text-white">
|
||||
{rep.sales}
|
||||
{fmtSales(rep.sales)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -144,3 +144,22 @@ export function financialOverview(projects = [], today) {
|
||||
}
|
||||
return { collected, outstandingAR, pendingPayouts, vendorSpend };
|
||||
}
|
||||
|
||||
// Top performers leaderboard — credits each project to its closer (canvasser/sales rep on the team),
|
||||
// ranked by sales (Σ contract value). Returns [{ id, name, deals, sales, commission }].
|
||||
export function repLeaderboard(projects = [], resolvePerson) {
|
||||
const by = {};
|
||||
for (const p of projects) {
|
||||
const members = p.teamMembers || [];
|
||||
const closer = members.find((t) => /Canvasser|Sales|Estimator/i.test(t.jobRole || ''))
|
||||
|| members.find((t) => /FIELD_AGENT|SALES/i.test(t.orgRole || ''));
|
||||
const id = closer?.userId || p.salesRepId;
|
||||
if (!id) continue;
|
||||
const name = closer?.name || resolvePerson?.(id)?.name || id;
|
||||
by[id] = by[id] || { id, name, deals: 0, sales: 0, commission: 0 };
|
||||
by[id].deals += 1;
|
||||
by[id].sales += Number(p.approvedBudget ?? p.budget ?? 0);
|
||||
by[id].commission += Number(p.commission || 0);
|
||||
}
|
||||
return Object.values(by).sort((a, b) => b.sales - a.sales);
|
||||
}
|
||||
|
||||
@@ -201,8 +201,8 @@ const Dashboard = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* --- COMPANY-WIDE ANALYTICS (ADMIN) --- */}
|
||||
{user?.role === 'ADMIN' && (
|
||||
{/* --- COMPANY-WIDE ANALYTICS (ADMIN + OWNER) --- */}
|
||||
{['ADMIN', 'OWNER'].includes(user?.role) && (
|
||||
<div className="space-y-6">
|
||||
{/* Section header */}
|
||||
<div className="flex items-center space-x-3">
|
||||
@@ -398,7 +398,7 @@ const Dashboard = () => {
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-zinc-900 dark:text-white font-semibold text-sm mb-0.5">{lead.propertyData.propertyAddress.split(',')[0]}</h4>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs">{lead.propertyData.propertyType} • ${lead.propertyData.currentEstimatedMarketValue.toLocaleString()}</p>
|
||||
<p className="text-zinc-500 dark:text-zinc-400 text-xs">{lead.propertyData.propertyType} • Home value ${lead.propertyData.currentEstimatedMarketValue.toLocaleString('en-US')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
|
||||
Reference in New Issue
Block a user