import React, { useEffect, useState, useMemo } from 'react'; import { useMockStore } from '../data/mockStore'; import { useAuth } from '../context/AuthContext'; import { Link } from 'react-router-dom'; import { DollarSign, Flame, Calendar, CloudRain, Wind, Droplets, Thermometer, MapPin, Clock, ArrowUpRight, Sun, Cloud, AlertTriangle, Loader2, ShieldCheck } 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'; import { TopSalesLeaderboard } from '../components/dashboard/TopSalesLeaderboard'; import { RevenueByNeighborhood } from '../components/dashboard/RevenueByNeighborhood'; import { GoldenLeadsScatter } from '../components/dashboard/GoldenLeadsScatter'; import { ActionCenterWidget } from '../components/dashboard/ActionCenterWidget'; import { WeatherRiskGauge } from '../components/dashboard/WeatherRiskGauge'; import AnimatedNumber from '../components/AnimatedNumber'; import Loader from '../components/Loader'; const Dashboard = () => { 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); return () => clearTimeout(timer); }, []); // --- Weather State (Lifted) --- const [weather, setWeather] = useState(null); const [forecast, setForecast] = useState([]); const [weatherLoading, setWeatherLoading] = useState(true); const [weatherError, setWeatherError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const fetchWeather = async () => { const API_KEY = config.weatherApiKey; const CITY = "Plano,US"; try { setWeatherLoading(true); if (config.isDemoMode(API_KEY)) { await new Promise(resolve => setTimeout(resolve, 800)); const mockCurrent = { name: "Plano", sys: { country: "US" }, main: { temp: 75, temp_max: 82, temp_min: 68, feels_like: 77, humidity: 45 }, weather: [{ id: 800, description: "Clear Sky (Demo)" }], wind: { speed: 8 } }; const mockForecast = Array.from({ length: 5 }).map((_, i) => ({ dt: Math.floor(Date.now() / 1000) + (i * 10800), main: { temp: 75 + (i % 2 === 0 ? 2 : -2) }, weather: [{ id: 800 + (i * 10) }] })); setWeather(mockCurrent); setForecast(mockForecast); setLastUpdated(new Date()); setWeatherLoading(false); setWeatherError(null); return; } const currentRes = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${CITY}&units=imperial&appid=${API_KEY}`); if (!currentRes.ok) throw new Error(currentRes.statusText); const currentData = await currentRes.json(); const forecastRes = await fetch(`https://api.openweathermap.org/data/2.5/forecast?q=${CITY}&units=imperial&appid=${API_KEY}`); if (!forecastRes.ok) throw new Error(forecastRes.statusText); const forecastData = await forecastRes.json(); setWeather(currentData); setForecast(forecastData.list?.slice(0, 5) || []); setLastUpdated(new Date()); setWeatherLoading(false); setWeatherError(null); } catch (err) { console.error("Weather fetch failed", err); setWeatherError(err.message); setWeatherLoading(false); } }; useEffect(() => { fetchWeather(); const interval = setInterval(fetchWeather, 600000); return () => clearInterval(interval); }, []); // Clock for Plano, TX const [currentTime, setCurrentTime] = useState(new Date()); useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 60000); // Every minute return () => clearInterval(timer); }, []); // Derived Metrics & Split Schedule const hotLeads = properties.filter(p => p.canvassingStatus === 'Hot Lead'); const allMyMeetings = meetings.filter(m => m.agentId === user?.id || m.agentId === 'e1'); const today = new Date(); today.setHours(0, 0, 0, 0); const upcomingMeetings = allMyMeetings .filter(m => new Date(m.date) >= today) .sort((a, b) => new Date(a.date) - new Date(b.date)); const pastMeetings = allMyMeetings .filter(m => new Date(m.date) < today) .sort((a, b) => new Date(b.date) - new Date(a.date)); // Most recent first if (isLoading) return ; return (
{/* Ambient Background Glows */}

Dashboard Overview

Welcome back, here's what's happening with your territory. {'Ѕ'}{'а'}{'t'}{'у'}{'а'}{'m'} {'R'}{'а'}{'ѕ'}{'t'}{'о'}{'g'}{'і'}

{/* Real-time Clock Widget */}
{currentTime.toLocaleTimeString('en-US', { timeZone: 'America/Chicago', hour: 'numeric', minute: '2-digit', hour12: false })}
{currentTime.toLocaleDateString('en-US', { timeZone: 'America/Chicago', weekday: 'long', month: 'long', day: 'numeric' })} • Plano, TX
{/* --- ADMIN ACTION CENTER --- */} {user?.role === 'ADMIN' && } {/* 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 */}
{/* Weather Risk - Gauge */}
{/* Live Weather Widget (Reused) */}
{/* --- ANALYTICS ROW 2: Opportunity & Intent --- */}
{/* Revenue by Neighborhood */}
{/* Top 6 Sales Reps Leaderboard */}
{/* --- ANALYTICS ROW 3: Golden Leads Scatter --- */}
{/* Main Content Grid (Leads & Schedule) */}
{/* Left Col: Hot Leads and Schedule */}
{/* Hot Leads Panel */}

Hot Leads

View Map
{hotLeads.length === 0 ? (
No hot leads active. Time to canvas!
) : ( hotLeads.slice(0, 5).map(lead => (
{lead.id}

{lead.propertyData.propertyAddress.split(',')[0]}

{lead.propertyData.propertyType} • ${lead.propertyData.currentEstimatedMarketValue.toLocaleString()}

Hot Lead
)) )}
{/* Right Col: Schedule */}

Upcoming

{upcomingMeetings.length === 0 ? (

No upcoming meetings.

) : ( upcomingMeetings.map(meeting => ( )) )}

History

{pastMeetings.length === 0 ? (

No past meetings.

) : ( pastMeetings.slice(0, 5).map(meeting => ( )) )}
igotsar.matyas | LynkedUpPro - Turns out roofing CRMs don't build themselves. Who knew?
); }; // --- Sub Components --- const MeetingRow = ({ meeting, type }) => (
{new Date(meeting.date).toLocaleDateString('en-US', { month: 'short' })} {new Date(meeting.date).getDate()}

{meeting.customerName}

{meeting.time}
{meeting.status}
); const MetricCard = ({ title, value, trend, icon: Icon, trendColor, bgGlow }) => (
{trend}

{title}

{/* Decorative Neomorphic Inner Shadow */}
); const RealWeatherWidget = ({ weather, forecast, loading, error, lastUpdated, onRetry }) => { // --- Icons Logic --- const getWeatherIcon = (code) => { if (code === 800) return ; if (code > 800) return ; if (code >= 500 && code < 600) return ; if (code >= 200 && code < 300) return ; return ; }; const getMiniIcon = (code) => { if (code === 800) return ; if (code > 800) return ; if (code >= 500) return ; return ; }; if (loading && !weather) return
Syncing OWM...
; if (error) return (

Weather Unavailable

{error}

{(error.includes('401') || error.includes('Invalid API key')) && (

Please configure your VITE_OPENWEATHER_API_KEY in the .env file.

)}
); if (!weather || !weather.main) return
Weather Unavailable
; return ( {/* Dynamic Weather Background Gradient - subtle */}
{weather.name || "Plano"}, {weather.sys?.country}

°F

Updated: {lastUpdated?.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}

{getWeatherIcon(weather.weather[0].id)}
Wind
mph
Humidity
%
{forecast.map((item, index) => { const date = new Date(item.dt * 1000); const displayTime = date.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true }); return (
{displayTime}
{getMiniIcon(item.weather[0].id)}
°
) })}
); }; export default Dashboard;