diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 1162e26..790d868 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -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 */}
{
/>
+ {/* --- COMPANY-WIDE ANALYTICS (ADMIN) --- */}
+ {user?.role === 'ADMIN' && (
+
+ {/* Section header */}
+
+
+
Company-Wide Analytics
+ Admin View
+
+
+ {/* Row 1: Pipeline + Win Rate + Storm */}
+
+
+ {/* Open Pipeline */}
+
+
+
Open Pipeline
+
+ {analytics.funnel.reduce((s, f) => s + f.count, 0)} leads
+
+
+
+ ${(analytics.pipeline / 1000).toFixed(0)}K
+
+ {/* Mini funnel bars */}
+
+ {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 (
+
+
{STAGE_LABELS[f.stage]}
+
+
{f.count}
+
+ );
+ })}
+
+
+
+ {/* Win Rate */}
+
+ Win Rate
+
+ {analytics.win.rate}
+ %
+
+
+
+ {analytics.win.won} Won
+ {analytics.win.decided} Decided
+ {analytics.win.lost} Lost
+
+
+
+ {/* Storm Attribution */}
+
+ Storm-Attributed Revenue
+
+ {analytics.storm.revenuePct}
+ %
+
+
+
+ {analytics.storm.stormLeadCount} Storm Leads
+ ${(analytics.storm.stormRevenue / 1000).toFixed(0)}K of ${(analytics.storm.totalRevenue / 1000).toFixed(0)}K
+
+
+
+
+ {/* Row 2: Revenue / Profit breakdown */}
+
+ P&L Summary
+
+
+
Revenue
+
${(analytics.rev.revenue / 1000).toFixed(0)}K
+
+
+
Gross Profit
+
= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'}`}>
+ ${(analytics.rev.grossProfit / 1000).toFixed(0)}K
+
+
+
+
Commissions
+
${(analytics.rev.commissions / 1000).toFixed(0)}K
+
+
+
Net Profit
+
= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-500 dark:text-red-400'}`}>
+ ${(analytics.rev.netProfit / 1000).toFixed(0)}K
+
+
+
+
+
+ )}
+
{/* --- ANALYTICS ROW 1: Territory Health & Risk --- */}
{/* Roof Condition - Donut */}
diff --git a/src/pages/owner/OwnerSnapshot.jsx b/src/pages/owner/OwnerSnapshot.jsx
index 48e9e4b..6d9931d 100644
--- a/src/pages/owner/OwnerSnapshot.jsx
+++ b/src/pages/owner/OwnerSnapshot.jsx
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext';
import { useMockStore } from '../../data/mockStore';
+import { pipelineValue, pipelineFunnel, winRate, revenueSummary, commissionByRep, stormAttribution } from '../../data/selectors';
import FinancialKPICards from '../../components/owner/FinancialKPICards';
import UrgentItemsPanel from '../../components/owner/UrgentItemsPanel';
import FinancialDetailsModal from '../../components/owner/FinancialDetailsModal';
@@ -26,6 +27,7 @@ const OwnerSnapshot = () => {
roleCommissionOverrides, orgMembers,
addJobTeamMember, removeJobTeamMember, updateJobTeamMember,
subcontractorTasks, subcontractors,
+ kanbanLeads, resolvePerson,
} = useMockStore();
// Find the current owner profile
@@ -38,6 +40,19 @@ const OwnerSnapshot = () => {
projects.filter(p => p.ownerId === user?.id)
, [projects, user]);
+ // Pure-analytics derived from selectors.js
+ const analytics = useMemo(() => ({
+ pipeline: pipelineValue(kanbanLeads),
+ funnel: pipelineFunnel(kanbanLeads),
+ win: winRate(kanbanLeads),
+ rev: revenueSummary(ownerProjects),
+ commissions: commissionByRep(ownerProjects, resolvePerson),
+ storm: stormAttribution(kanbanLeads, ownerProjects),
+ }), [kanbanLeads, ownerProjects, resolvePerson]);
+
+ // Stage display labels for the pipeline funnel
+ const STAGE_LABELS = { kc_new: 'New', kc_contacted: 'Contacted', kc_appt: 'Appt', kc_estimate: 'Estimate' };
+
// Modal States
const [financialModal, setFinancialModal] = useState({ isOpen: false, type: 'revenue' });
const [actionModal, setActionModal] = useState({ isOpen: false, filter: 'all' });
@@ -217,6 +232,130 @@ const OwnerSnapshot = () => {
+ {/* ── Pipeline Analytics ── */}
+
+
+
+ Pipeline Analytics
+
+
+ {/* Pipeline KPI tiles */}
+
+ {[
+ { label: 'Open Pipeline', value: formatCurrency(analytics.pipeline), color: 'text-blue-500', bg: 'bg-blue-50 dark:bg-blue-500/10' },
+ { label: 'Win Rate', value: `${analytics.win.rate}%`, color: 'text-emerald-500', bg: 'bg-emerald-50 dark:bg-emerald-500/10' },
+ { label: 'Recognized Revenue', value: formatCurrency(analytics.rev.revenue), color: 'text-violet-500', bg: 'bg-violet-50 dark:bg-violet-500/10' },
+ { label: 'Gross Profit', value: formatCurrency(analytics.rev.grossProfit), color: 'text-amber-500', bg: 'bg-amber-50 dark:bg-amber-500/10' },
+ { label: 'Net Profit', value: formatCurrency(analytics.rev.netProfit), color: 'text-teal-500', bg: 'bg-teal-50 dark:bg-teal-500/10' },
+ ].map((kpi, i) => (
+
+
+ {kpi.label}
+
+ {kpi.value}
+
+ ))}
+
+
+ {/* Pipeline funnel + Commission by Rep + Storm attribution */}
+
+
+ {/* Pipeline Funnel */}
+
+ Pipeline Funnel
+ Pre-sale leads by stage
+
+ {analytics.funnel.map((row) => {
+ const maxVal = Math.max(...analytics.funnel.map(r => r.value), 1);
+ const pct = Math.round((row.value / maxVal) * 100);
+ return (
+
+
+ {STAGE_LABELS[row.stage] || row.stage}
+
+ {row.count} lead{row.count !== 1 ? 's' : ''} · {formatCurrency(row.value)}
+
+
+
+
+ );
+ })}
+
+
+
+ {/* Commission by Rep */}
+
+ Commission by Rep
+ Earned on owner projects
+
+ {analytics.commissions.length > 0 ? analytics.commissions.map((rep, i) => (
+
+
+
+ {i + 1}
+
+ {rep.name}
+
+
{formatCurrency(rep.total)}
+
+ )) : (
+
No commission data for current projects.
+ )}
+
+
+
+ {/* Storm Attribution */}
+
+ Storm Attribution
+ Storm-sourced share of pipeline & revenue
+
+
+
+ Pipeline
+ {analytics.storm.pipelinePct}% storm
+
+
+
+ {formatCurrency(analytics.storm.stormPipeline)} storm
+ {formatCurrency(analytics.storm.totalPipeline)} total
+
+
+
+
+ Revenue
+ {analytics.storm.revenuePct}% storm
+
+
+
+ {formatCurrency(analytics.storm.stormRevenue)} storm
+ {formatCurrency(analytics.storm.totalRevenue)} total
+
+
+
+
+
{analytics.storm.stormLeadCount} storm lead{analytics.storm.stormLeadCount !== 1 ? 's' : ''} tracked
+
+
+
+
+
+
+
{/* Charts Row */}