feat(dashboards): wire owner + admin KPIs to computed selectors (pipeline, funnel, win-rate, revenue, commission, storm)

This commit is contained in:
Satyam Rastogi
2026-05-29 16:50:19 +05:30
parent 8f5a32179d
commit dd781f4692
2 changed files with 274 additions and 5 deletions
+135 -5
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { useMockStore } from '../data/mockStore';
import { useAuth } from '../context/AuthContext';
import { Link } from 'react-router-dom';
@@ -8,6 +8,7 @@ import {
} from 'lucide-react';
import { SpotlightCard } from '../components/SpotlightCard';
import { config } from '../config/env';
import { pipelineValue, pipelineFunnel, winRate, revenueSummary, stormAttribution } from '../data/selectors';
// New Widget Imports
import { RoofConditionChart } from '../components/dashboard/RoofConditionChart';
@@ -21,10 +22,19 @@ import AnimatedNumber from '../components/AnimatedNumber';
import Loader from '../components/Loader';
const Dashboard = () => {
const { properties, meetings } = useMockStore();
const { properties, meetings, projects, kanbanLeads, resolvePerson } = useMockStore();
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(true);
// Company-wide analytics (admin sees ALL projects + ALL kanbanLeads — no owner scoping)
const analytics = useMemo(() => ({
pipeline: pipelineValue(kanbanLeads),
funnel: pipelineFunnel(kanbanLeads),
win: winRate(kanbanLeads),
rev: revenueSummary(projects),
storm: stormAttribution(kanbanLeads, projects),
}), [kanbanLeads, projects]);
useEffect(() => {
// Simulate initial data fetch
const timer = setTimeout(() => setIsLoading(false), 800);
@@ -155,9 +165,9 @@ const Dashboard = () => {
{/* Top Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<MetricCard
title="Volume Closed"
value="$4.2M"
trend="+12% vs last month"
title="Recognized Revenue"
value={`$${(analytics.rev.revenue / 1000).toFixed(0)}K`}
trend={`Gross profit $${(analytics.rev.grossProfit / 1000).toFixed(0)}K`}
icon={DollarSign}
trendColor="text-emerald-600 dark:text-emerald-300"
bgGlow="bg-emerald-500/20"
@@ -188,6 +198,126 @@ const Dashboard = () => {
/>
</div>
{/* --- COMPANY-WIDE ANALYTICS (ADMIN) --- */}
{user?.role === 'ADMIN' && (
<div className="space-y-6">
{/* Section header */}
<div className="flex items-center space-x-3">
<ShieldCheck size={18} className="text-blue-500" />
<h2 className="text-lg font-bold text-zinc-900 dark:text-white/90 tracking-tight">Company-Wide Analytics</h2>
<span className="text-[10px] uppercase font-bold tracking-wider text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-white/5 px-2.5 py-1 rounded-full border border-zinc-200 dark:border-white/10">Admin View</span>
</div>
{/* Row 1: Pipeline + Win Rate + Storm */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Open Pipeline */}
<SpotlightCard className="p-6">
<div className="flex justify-between items-start mb-4">
<p className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Open Pipeline</p>
<span className="text-[10px] uppercase font-bold tracking-wider text-blue-600 dark:text-blue-300 bg-blue-50 dark:bg-blue-500/10 px-2.5 py-1 rounded-full border border-blue-100 dark:border-blue-500/20">
{analytics.funnel.reduce((s, f) => s + f.count, 0)} leads
</span>
</div>
<div className="text-3xl font-extrabold text-zinc-900 dark:text-white tracking-tight mb-4">
${(analytics.pipeline / 1000).toFixed(0)}K
</div>
{/* Mini funnel bars */}
<div className="space-y-2">
{analytics.funnel.map((f) => {
const STAGE_LABELS = {
kc_new: 'New',
kc_contacted: 'Contacted',
kc_appt: 'Appointment',
kc_estimate: 'Estimate',
};
const maxCount = Math.max(...analytics.funnel.map(s => s.count), 1);
const pct = Math.round((f.count / maxCount) * 100);
return (
<div key={f.stage} className="flex items-center space-x-2">
<span className="text-[10px] text-zinc-500 dark:text-zinc-400 w-20 shrink-0 font-medium">{STAGE_LABELS[f.stage]}</span>
<div className="flex-1 bg-zinc-100 dark:bg-white/5 rounded-full h-1.5 border border-zinc-200 dark:border-white/5">
<div
className="h-1.5 rounded-full bg-gradient-to-r from-blue-500 to-blue-400"
style={{ width: `${pct}%` }}
/>
</div>
<span className="text-[10px] font-bold text-zinc-600 dark:text-zinc-300 w-4 text-right">{f.count}</span>
</div>
);
})}
</div>
</SpotlightCard>
{/* Win Rate */}
<SpotlightCard className="p-6">
<p className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4">Win Rate</p>
<div className="flex items-end space-x-2 mb-4">
<span className="text-5xl font-extrabold text-zinc-900 dark:text-white tracking-tighter">{analytics.win.rate}</span>
<span className="text-xl font-bold text-zinc-400 dark:text-zinc-500 mb-1">%</span>
</div>
<div className="w-full bg-zinc-100 dark:bg-white/5 rounded-full h-2 border border-zinc-200 dark:border-white/5 mb-4">
<div
className="h-2 rounded-full bg-gradient-to-r from-emerald-500 to-emerald-400"
style={{ width: `${analytics.win.rate}%` }}
/>
</div>
<div className="flex justify-between text-[10px] font-bold uppercase tracking-wider">
<span className="text-emerald-600 dark:text-emerald-400">{analytics.win.won} Won</span>
<span className="text-zinc-400 dark:text-zinc-500">{analytics.win.decided} Decided</span>
<span className="text-red-500 dark:text-red-400">{analytics.win.lost} Lost</span>
</div>
</SpotlightCard>
{/* Storm Attribution */}
<SpotlightCard className="p-6">
<p className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-4">Storm-Attributed Revenue</p>
<div className="flex items-end space-x-2 mb-4">
<span className="text-5xl font-extrabold text-zinc-900 dark:text-white tracking-tighter">{analytics.storm.revenuePct}</span>
<span className="text-xl font-bold text-zinc-400 dark:text-zinc-500 mb-1">%</span>
</div>
<div className="w-full bg-zinc-100 dark:bg-white/5 rounded-full h-2 border border-zinc-200 dark:border-white/5 mb-4">
<div
className="h-2 rounded-full bg-gradient-to-r from-sky-500 to-cyan-400"
style={{ width: `${analytics.storm.revenuePct}%` }}
/>
</div>
<div className="flex justify-between text-[10px] font-bold uppercase tracking-wider">
<span className="text-sky-600 dark:text-sky-400">{analytics.storm.stormLeadCount} Storm Leads</span>
<span className="text-zinc-400 dark:text-zinc-500">${(analytics.storm.stormRevenue / 1000).toFixed(0)}K of ${(analytics.storm.totalRevenue / 1000).toFixed(0)}K</span>
</div>
</SpotlightCard>
</div>
{/* Row 2: Revenue / Profit breakdown */}
<SpotlightCard className="p-6">
<p className="text-xs font-bold uppercase tracking-widest text-zinc-500 dark:text-zinc-400 mb-5">P&amp;L Summary</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<p className="text-[10px] uppercase font-bold tracking-widest text-zinc-400 dark:text-zinc-500 mb-1">Revenue</p>
<p className="text-2xl font-extrabold text-zinc-900 dark:text-white">${(analytics.rev.revenue / 1000).toFixed(0)}K</p>
</div>
<div>
<p className="text-[10px] uppercase font-bold tracking-widest text-zinc-400 dark:text-zinc-500 mb-1">Gross Profit</p>
<p className={`text-2xl font-extrabold ${analytics.rev.grossProfit >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'}`}>
${(analytics.rev.grossProfit / 1000).toFixed(0)}K
</p>
</div>
<div>
<p className="text-[10px] uppercase font-bold tracking-widest text-zinc-400 dark:text-zinc-500 mb-1">Commissions</p>
<p className="text-2xl font-extrabold text-amber-600 dark:text-amber-400">${(analytics.rev.commissions / 1000).toFixed(0)}K</p>
</div>
<div>
<p className="text-[10px] uppercase font-bold tracking-widest text-zinc-400 dark:text-zinc-500 mb-1">Net Profit</p>
<p className={`text-2xl font-extrabold ${analytics.rev.netProfit >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'}`}>
${(analytics.rev.netProfit / 1000).toFixed(0)}K
</p>
</div>
</div>
</SpotlightCard>
</div>
)}
{/* --- ANALYTICS ROW 1: Territory Health & Risk --- */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Roof Condition - Donut */}