diff --git a/src/components/dispatch/DispatchActivityLog.jsx b/src/components/dispatch/DispatchActivityLog.jsx
new file mode 100644
index 0000000..d8d28ef
--- /dev/null
+++ b/src/components/dispatch/DispatchActivityLog.jsx
@@ -0,0 +1,213 @@
+import React, { useState, useEffect } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { History, ChevronDown, Zap } from 'lucide-react';
+
+// ---------------------------------------------------------------------------
+// Config
+// ---------------------------------------------------------------------------
+const REP_STATUS_COLORS = { available: '#10B981', en_route: '#3B82F6', busy: '#F59E0B' };
+
+const URGENCY_COLORS = {
+ emergency: '#EF4444',
+ high: '#F59E0B',
+ standard: '#6B7280',
+};
+
+const LEAD_TYPE_SHORT = {
+ emergency_tarp: 'Emerg. Tarp',
+ roof_inspection: 'Inspection',
+ insurance_claim: 'Insurance',
+ retail_estimate: 'Estimate',
+};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+function timeAgo(ts) {
+ const secs = Math.floor((Date.now() - ts) / 1000);
+ if (secs < 10) return 'Just now';
+ if (secs < 60) return `${secs}s ago`;
+ const mins = Math.floor(secs / 60);
+ if (mins < 60) return `${mins} min ago`;
+ return `${Math.floor(mins / 60)}h ago`;
+}
+
+// ---------------------------------------------------------------------------
+// Single log entry row
+// ---------------------------------------------------------------------------
+const LogEntry = ({ entry, stormMode }) => {
+ const repColor = REP_STATUS_COLORS[entry.repStatus] || '#6B7280';
+ const urgColor = URGENCY_COLORS[entry.urgency] || '#6B7280';
+ const isEmerg = entry.urgency === 'emergency';
+
+ return (
+
+ {/* Rep avatar */}
+
+ {entry.repInitials}
+
+
+ {/* Dispatch info */}
+
+
+
+ {entry.repFirstName}
+
+ →
+
+ {entry.customerName}
+
+
+
+ {entry.address}
+
+ {LEAD_TYPE_SHORT[entry.leadType] || entry.leadType}
+
+
+
+
+ {/* Timestamp */}
+
+ {timeAgo(entry.timestamp)}
+
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+const DispatchActivityLog = ({ log, accent, stormMode }) => {
+ const [open, setOpen] = useState(false);
+ const [tick, setTick] = useState(0);
+
+ // Refresh relative timestamps every 30s
+ useEffect(() => {
+ const t = setInterval(() => setTick(n => n + 1), 30000);
+ return () => clearInterval(t);
+ }, []);
+
+ // Auto-expand when first entry arrives
+ useEffect(() => {
+ if (log.length === 1) setOpen(true);
+ }, [log.length]);
+
+ return (
+
+
+ {/* ── Section header (always visible) ── */}
+
+
+ {/* ── Expandable log body ── */}
+
+ {open && (
+
+
+
+ {log.length === 0 ? (
+ /* Empty state */
+
+
+
+
+
+ Assign a lead to start logging
+
+
+ ) : (
+ /* Log entries — newest at top, max-height scroll */
+
+
+ {log.map(entry => (
+
+ ))}
+
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default DispatchActivityLog;
diff --git a/src/components/dispatch/DispatchChartModal.jsx b/src/components/dispatch/DispatchChartModal.jsx
new file mode 100644
index 0000000..bfad796
--- /dev/null
+++ b/src/components/dispatch/DispatchChartModal.jsx
@@ -0,0 +1,405 @@
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import { motion } from 'framer-motion';
+import {
+ ComposedChart, Area, Line, XAxis, YAxis,
+ CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine,
+} from 'recharts';
+import { X, BarChart2, Users } from 'lucide-react';
+import { useTheme } from '../../context/ThemeContext';
+import { DISPATCH_REPS, DISPATCH_WEEKLY_STATS } from '../../data/mockStore';
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+const REP_STATUS_COLORS = { available: '#10B981', en_route: '#3B82F6', busy: '#F59E0B' };
+
+// ---------------------------------------------------------------------------
+// Custom Recharts dot — highlights storm day, today, emergency days
+// ---------------------------------------------------------------------------
+const makeAreaDot = (areaColor) => (props) => {
+ const { cx, cy, payload } = props;
+ if (cx === undefined || cy === undefined) return null;
+ if (payload.isToday) {
+ return (
+
+
+
+
+ );
+ }
+ if (payload.stormDay) {
+ return (
+
+
+
+
+ );
+ }
+ if (payload.emergency > 0) {
+ return ;
+ }
+ return ;
+};
+
+// ---------------------------------------------------------------------------
+// Custom Tooltip
+// ---------------------------------------------------------------------------
+const ChartTooltip = ({ active, payload, isDark }) => {
+ if (!active || !payload?.length) return null;
+ const d = payload[0]?.payload;
+ if (!d) return null;
+
+ const bg = isDark ? '#1c1c1e' : '#ffffff';
+ const borderColor = d.stormDay ? '#F59E0B' : d.isToday ? '#3B82F6' : (isDark ? 'rgba(255,255,255,0.1)' : '#e4e4e7');
+ const textPrimary = isDark ? '#f4f4f5' : '#18181b';
+ const textMuted = isDark ? '#71717a' : '#a1a1aa';
+
+ return (
+
+
+ {d.date}
+ {d.stormDay && (
+
+ Storm Day
+
+ )}
+ {d.isToday && (
+
+ Today
+
+ )}
+
+
+
+ {d.emergency > 0 && (
+
+ )}
+
+ );
+};
+
+const TooltipRow = ({ label, value, color, textMuted }) => (
+
+ {label}
+ {value}
+
+);
+
+// ---------------------------------------------------------------------------
+// Summary stat chip
+// ---------------------------------------------------------------------------
+const StatChip = ({ label, value, color }) => (
+
+ {value}
+ {label}
+
+);
+
+// ---------------------------------------------------------------------------
+// Chart legend item
+// ---------------------------------------------------------------------------
+const LegendDash = ({ color, dashed }) => (
+
+
+ {dashed &&
}
+ {dashed &&
}
+
+);
+
+// ---------------------------------------------------------------------------
+// Rep performance leaderboard
+// ---------------------------------------------------------------------------
+const RepLeaderboard = () => {
+ const repTotals = DISPATCH_REPS.map(rep => ({
+ ...rep,
+ weekDispatched: DISPATCH_WEEKLY_STATS.reduce((s, d) => s + (d.repBreakdown[rep.id] || 0), 0),
+ })).sort((a, b) => b.weekDispatched - a.weekDispatched);
+
+ const max = repTotals[0]?.weekDispatched || 1;
+
+ return (
+
+
+
+
+ Rep Performance This Week
+
+
+
+ {repTotals.map((rep, i) => {
+ const color = REP_STATUS_COLORS[rep.status] || '#6B7280';
+ const pct = (rep.weekDispatched / max) * 100;
+ return (
+
+ {/* Rank */}
+
+ #{i + 1}
+
+ {/* Avatar */}
+
+ {rep.initials}
+
+ {/* Name */}
+
+ {rep.name.split(' ')[0]}
+
+ {/* Bar */}
+
+
+
+ {/* Count */}
+
+ {rep.weekDispatched}
+
+ {/* Rating */}
+
+ ★{rep.rating}
+
+
+ );
+ })}
+
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+const DispatchChartModal = ({ accent, stormMode, onClose }) => {
+ const { isDark } = useTheme();
+
+ // ESC key
+ useEffect(() => {
+ const handler = (e) => { if (e.key === 'Escape') onClose(); };
+ document.addEventListener('keydown', handler);
+ return () => document.removeEventListener('keydown', handler);
+ }, [onClose]);
+
+ // Derived summary stats
+ const totalDispatched = DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.dispatched, 0);
+ const avgWaitOverall = Math.round(
+ DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.avgWait, 0) / DISPATCH_WEEKLY_STATS.length
+ );
+ const totalEmergency = DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.emergency, 0);
+
+ const areaColor = stormMode ? '#F59E0B' : '#3B82F6';
+ const gridColor = isDark ? 'rgba(255,255,255,0.05)' : '#f0f0f0';
+ const tickColor = isDark ? '#52525b' : '#a1a1aa';
+
+ const renderAreaDot = makeAreaDot(areaColor);
+
+ return ReactDOM.createPortal(
+ <>
+ {/* Backdrop */}
+
+
+ {/* Modal panel */}
+
+
+ {/* ── Modal header ── */}
+
+
+
+
+
+
+
+ Weekly Performance
+
+
+ Mar 20 – Mar 26, 2026
+
+
+
+
+
+
+
+ {/* ── Scrollable body ── */}
+
+
+ {/* Summary stat chips */}
+
+
+ {/* Chart */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `${v}m`}
+ />
+
+ }
+ cursor={{
+ stroke: isDark ? 'rgba(255,255,255,0.08)' : '#e4e4e7',
+ strokeWidth: 1,
+ strokeDasharray: '4 3',
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {/* Legend */}
+
+
+
+ Dispatched
+
+
+
+ Avg Wait
+
+
+
+
+
+
+
+ {/* Rep leaderboard */}
+
+
+
+
+ >,
+ document.body
+ );
+};
+
+export default DispatchChartModal;
diff --git a/src/components/dispatch/DispatchLogDrawer.jsx b/src/components/dispatch/DispatchLogDrawer.jsx
new file mode 100644
index 0000000..0079a8b
--- /dev/null
+++ b/src/components/dispatch/DispatchLogDrawer.jsx
@@ -0,0 +1,211 @@
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import { motion } from 'framer-motion';
+import { X, History, Zap } from 'lucide-react';
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+const REP_STATUS_COLORS = { available: '#10B981', en_route: '#3B82F6', busy: '#F59E0B' };
+
+const URGENCY_COLORS = {
+ emergency: '#EF4444',
+ high: '#F59E0B',
+ standard: '#6B7280',
+};
+
+const LEAD_TYPE_SHORT = {
+ emergency_tarp: 'Emerg. Tarp',
+ roof_inspection: 'Inspection',
+ insurance_claim: 'Insurance',
+ retail_estimate: 'Estimate',
+};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+function timeAgo(ts) {
+ const secs = Math.floor((Date.now() - ts) / 1000);
+ if (secs < 10) return 'Just now';
+ if (secs < 60) return `${secs}s ago`;
+ const mins = Math.floor(secs / 60);
+ if (mins < 60) return `${mins} min ago`;
+ return `${Math.floor(mins / 60)}h ago`;
+}
+
+// ---------------------------------------------------------------------------
+// Single log entry row
+// ---------------------------------------------------------------------------
+const LogEntry = ({ entry, stormMode }) => {
+ const repColor = REP_STATUS_COLORS[entry.repStatus] || '#6B7280';
+ const urgColor = URGENCY_COLORS[entry.urgency] || '#6B7280';
+ const isEmerg = entry.urgency === 'emergency';
+
+ return (
+
+ {/* Rep avatar */}
+
+ {entry.repInitials}
+
+
+ {/* Dispatch info */}
+
+
+
+ {entry.repFirstName}
+
+ →
+
+ {entry.customerName}
+
+
+
+ {entry.address}
+
+ {LEAD_TYPE_SHORT[entry.leadType] || entry.leadType}
+
+
+
+
+ {/* Timestamp */}
+
+ {timeAgo(entry.timestamp)}
+
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Empty state
+// ---------------------------------------------------------------------------
+const EmptyState = ({ accent }) => (
+
+
+
+
+
+
No activity yet
+
+ Assign a lead to start logging dispatches
+
+
+
+);
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+const DispatchLogDrawer = ({ log, accent, stormMode, onClose }) => {
+ // ESC key
+ useEffect(() => {
+ const handler = (e) => { if (e.key === 'Escape') onClose(); };
+ document.addEventListener('keydown', handler);
+ return () => document.removeEventListener('keydown', handler);
+ }, [onClose]);
+
+ return ReactDOM.createPortal(
+ <>
+ {/* Backdrop */}
+
+
+ {/* Drawer */}
+
+ {/* ── Header ── */}
+
+
+
+
+
+
+
+ Dispatch Log
+
+ {log.length > 0 && (
+
+ {log.length}
+
+ )}
+
+
+
+
+
+
+ {/* ── Log entries — full height, scrollable ── */}
+
+ {log.length === 0 ? (
+
+ ) : (
+ log.map(entry => (
+
+ ))
+ )}
+
+
+ {/* ── Footer ── */}
+ {log.length > 0 && (
+
+
+ Showing {log.length} of {log.length} entries · Newest first
+
+
+ )}
+
+ >,
+ document.body
+ );
+};
+
+export default DispatchLogDrawer;
diff --git a/src/components/dispatch/DispatchWeeklyChart.jsx b/src/components/dispatch/DispatchWeeklyChart.jsx
new file mode 100644
index 0000000..e45e20a
--- /dev/null
+++ b/src/components/dispatch/DispatchWeeklyChart.jsx
@@ -0,0 +1,416 @@
+import React, { useState } from 'react';
+import { motion, AnimatePresence } from 'framer-motion';
+import {
+ ComposedChart, Area, Line, XAxis, YAxis,
+ CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine,
+} from 'recharts';
+import { BarChart2, ChevronDown, Users } from 'lucide-react';
+import { useTheme } from '../../context/ThemeContext';
+import { DISPATCH_REPS, DISPATCH_WEEKLY_STATS } from '../../data/mockStore';
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+const REP_STATUS_COLORS = { available: '#10B981', en_route: '#3B82F6', busy: '#F59E0B' };
+
+// ---------------------------------------------------------------------------
+// Custom Recharts dot — highlights storm day, today, and emergency days
+// ---------------------------------------------------------------------------
+const makeAreaDot = (areaColor) => (props) => {
+ const { cx, cy, payload } = props;
+ if (cx === undefined || cy === undefined) return null;
+ if (payload.isToday) {
+ return (
+
+
+
+
+ );
+ }
+ if (payload.stormDay) {
+ return (
+
+
+
+
+ );
+ }
+ if (payload.emergency > 0) {
+ return ;
+ }
+ return ;
+};
+
+// ---------------------------------------------------------------------------
+// Custom Tooltip
+// ---------------------------------------------------------------------------
+const ChartTooltip = ({ active, payload, isDark }) => {
+ if (!active || !payload?.length) return null;
+ const d = payload[0]?.payload;
+ if (!d) return null;
+
+ const bg = isDark ? '#1c1c1e' : '#ffffff';
+ const borderColor = d.stormDay ? '#F59E0B' : d.isToday ? '#3B82F6' : (isDark ? 'rgba(255,255,255,0.1)' : '#e4e4e7');
+ const textPrimary = isDark ? '#f4f4f5' : '#18181b';
+ const textMuted = isDark ? '#71717a' : '#a1a1aa';
+
+ return (
+
+ {/* Header */}
+
+ {d.date}
+ {d.stormDay && (
+
+ Storm Day
+
+ )}
+ {d.isToday && (
+
+ Today
+
+ )}
+
+ {/* Rows */}
+
+
+ {d.emergency > 0 && (
+
+ )}
+
+ );
+};
+
+const TooltipRow = ({ label, value, color, textMuted }) => (
+
+ {label}
+ {value}
+
+);
+
+// ---------------------------------------------------------------------------
+// Summary stat chip (top of expanded panel)
+// ---------------------------------------------------------------------------
+const StatChip = ({ label, value, color }) => (
+
+ {value}
+ {label}
+
+);
+
+// ---------------------------------------------------------------------------
+// Chart legend row
+// ---------------------------------------------------------------------------
+const LegendDash = ({ color, dashed }) => (
+
+
+ {dashed &&
}
+ {dashed &&
}
+
+);
+
+// ---------------------------------------------------------------------------
+// Rep performance leaderboard
+// ---------------------------------------------------------------------------
+const RepLeaderboard = ({ open }) => {
+ const repTotals = DISPATCH_REPS.map(rep => ({
+ ...rep,
+ weekDispatched: DISPATCH_WEEKLY_STATS.reduce((s, d) => s + (d.repBreakdown[rep.id] || 0), 0),
+ })).sort((a, b) => b.weekDispatched - a.weekDispatched);
+
+ const max = repTotals[0]?.weekDispatched || 1;
+
+ return (
+
+
+
+
+ Rep Performance This Week
+
+
+
+ {repTotals.map((rep, i) => {
+ const color = REP_STATUS_COLORS[rep.status] || '#6B7280';
+ const pct = (rep.weekDispatched / max) * 100;
+ return (
+
+ {/* Rank */}
+
+ {i + 1}
+
+ {/* Avatar */}
+
+ {rep.initials}
+
+ {/* Name */}
+
+ {rep.name.split(' ')[0]}
+
+ {/* Bar */}
+
+
+
+ {/* Count */}
+
+ {rep.weekDispatched}
+
+ {/* Rating */}
+
+ ★{rep.rating}
+
+
+ );
+ })}
+
+
+ );
+};
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+const DispatchWeeklyChart = ({ accent, stormMode }) => {
+ const { isDark } = useTheme();
+ const [open, setOpen] = useState(false);
+
+ // Summary stats derived from static data
+ const totalDispatched = DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.dispatched, 0);
+ const avgWaitOverall = Math.round(
+ DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.avgWait, 0) / DISPATCH_WEEKLY_STATS.length
+ );
+ const totalEmergency = DISPATCH_WEEKLY_STATS.reduce((s, d) => s + d.emergency, 0);
+
+ const areaColor = stormMode ? '#F59E0B' : '#3B82F6';
+ const gridColor = isDark ? 'rgba(255,255,255,0.05)' : '#f0f0f0';
+ const tickColor = isDark ? '#52525b' : '#a1a1aa';
+
+ const renderAreaDot = makeAreaDot(areaColor);
+
+ return (
+
+
+ {/* ── Section header (always visible) ── */}
+
+
+ {/* ── Expandable body ── */}
+
+ {open && (
+
+
+
+ {/* ── Summary stats strip ── */}
+
+
+ {/* ── Chart ── */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Left Y — dispatched count */}
+
+
+ {/* Right Y — avg wait in minutes */}
+ `${v}m`}
+ />
+
+ }
+ cursor={{
+ stroke: isDark ? 'rgba(255,255,255,0.08)' : '#e4e4e7',
+ strokeWidth: 1,
+ strokeDasharray: '4 3',
+ }}
+ />
+
+ {/* Storm day vertical reference */}
+
+
+ {/* Area — dispatched leads */}
+
+
+ {/* Line — avg wait time */}
+
+
+
+
+
+ {/* ── Legend ── */}
+
+
+
+ Dispatched
+
+
+
+ Avg Wait
+
+
+
+
+
+
+
+ {/* ── Rep leaderboard ── */}
+
+
+
+ )}
+
+
+ );
+};
+
+export default DispatchWeeklyChart;