diff --git a/src/App.jsx b/src/App.jsx index c1bfb3c..7a6ffad 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,6 +31,7 @@ import ContractorDashboard from './pages/contractor/ContractorDashboard'; import SubContractorDashboard from './pages/subcontractor/SubContractorDashboard'; import CreateLeadPage from './pages/CreateLeadPage'; import LeadsListPage from './pages/LeadsListPage'; +import LynkDispatchPage from './pages/LynkDispatchPage'; // ... (existing imports) const ProtectedRoute = ({ children, allowedRoles }) => { @@ -197,6 +198,22 @@ function App() { } /> + + + + } + /> + + + + } + /> { { to: "/admin/dashboard", icon: LayoutDashboard, label: "Dashboard" }, { to: "/owner/projects", icon: Briefcase, label: "Projects" }, { to: "/emp/fa/leads", icon: ClipboardList, label: "Leads" }, + { to: "/admin/dispatch", icon: Zap, label: "LynkDispatch" }, { to: "/owner/maps", icon: Map, label: "Territory Map" }, { to: "/owner/pro-canvas", @@ -155,6 +156,7 @@ const Layout = () => { { to: "/admin/schedule", icon: Calendar, label: "Schedule" }, { to: "/admin/leaderboard", icon: Trophy, label: "Leaderboard" }, { to: "/emp/fa/leads", icon: ClipboardList, label: "Leads" }, + { to: "/admin/dispatch", icon: Zap, label: "LynkDispatch" }, { to: "/admin/maps", icon: Map, label: "Territory Map" }, { to: "/admin/pro-canvas", diff --git a/src/data/mockStore.jsx b/src/data/mockStore.jsx index fd9f85d..acb2171 100644 --- a/src/data/mockStore.jsx +++ b/src/data/mockStore.jsx @@ -2720,6 +2720,339 @@ const MOCK_PROJECTS = [ } ]; +// --------------------------------------------------------------------------- +// LynkDispatch AI — Mock Data +// --------------------------------------------------------------------------- + +export const DISPATCH_REPS = [ + { + id: 'REP-01', name: 'Carlos Mendez', initials: 'CM', + status: 'en_route', // available | en_route | busy + currentLat: 33.0612, currentLng: -96.7891, + todayAppointments: 3, maxDaily: 6, + skills: ['insurance_claim', 'retail_estimate'], + performanceScore: 94, + }, + { + id: 'REP-02', name: 'Aisha Kumar', initials: 'AK', + status: 'available', + currentLat: 33.0812, currentLng: -96.7234, + todayAppointments: 2, maxDaily: 6, + skills: ['roof_inspection', 'emergency_tarp'], + performanceScore: 88, + }, + { + id: 'REP-03', name: 'Tom Bradley', initials: 'TB', + status: 'busy', + currentLat: 33.0389, currentLng: -96.7401, + todayAppointments: 5, maxDaily: 6, + skills: ['retail_estimate', 'roof_inspection'], + performanceScore: 79, + }, + { + id: 'REP-04', name: 'Nina Patel', initials: 'NP', + status: 'available', + currentLat: 33.0516, currentLng: -96.7512, + todayAppointments: 1, maxDaily: 6, + skills: ['insurance_claim', 'roof_inspection', 'emergency_tarp'], + performanceScore: 91, + }, + { + id: 'REP-05', name: 'Derek Walsh', initials: 'DW', + status: 'en_route', + currentLat: 33.0198, currentLng: -96.7234, + todayAppointments: 4, maxDaily: 6, + skills: ['retail_estimate', 'roof_inspection'], + performanceScore: 82, + }, +]; + +const DISPATCH_LEADS_INITIAL = [ + { + id: 'LD-1001', source: 'call_center', leadType: 'emergency_tarp', urgency: 'emergency', + customer: { name: 'John Smith', phone: '(972) 555-1001' }, + property: { address: '2612 Dunwick Dr', city: 'Plano', state: 'TX', zip: '75023', lat: 33.0708, lng: -96.7456 }, + estimatedDuration: 60, status: 'unassigned', arrivedMinsAgo: 8, assignedRepId: null, + }, + { + id: 'LD-1002', source: 'website_form', leadType: 'roof_inspection', urgency: 'high', + customer: { name: 'Maria Gonzalez', phone: '(972) 555-1002' }, + property: { address: '4817 Shady Brook Ln', city: 'Plano', state: 'TX', zip: '75093', lat: 33.0652, lng: -96.8021 }, + estimatedDuration: 45, status: 'unassigned', arrivedMinsAgo: 14, assignedRepId: null, + }, + { + id: 'LD-1003', source: 'canvassing', leadType: 'insurance_claim', urgency: 'high', + customer: { name: 'Robert Chen', phone: '(972) 555-1003' }, + property: { address: '1234 Oak Creek Blvd', city: 'Plano', state: 'TX', zip: '75075', lat: 33.0421, lng: -96.7138 }, + estimatedDuration: 45, status: 'assigned', arrivedMinsAgo: 22, assignedRepId: 'REP-01', + }, + { + id: 'LD-1004', source: 'referral', leadType: 'retail_estimate', urgency: 'standard', + customer: { name: 'Sarah Mitchell', phone: '(972) 555-1004' }, + property: { address: '890 Custer Rd', city: 'Plano', state: 'TX', zip: '75075', lat: 33.0389, lng: -96.7334 }, + estimatedDuration: 30, status: 'confirmed', arrivedMinsAgo: 35, assignedRepId: 'REP-03', + }, + { + id: 'LD-1005', source: 'call_center', leadType: 'roof_inspection', urgency: 'standard', + customer: { name: 'James Wilson', phone: '(972) 555-1005' }, + property: { address: '321 Independence Pkwy', city: 'Plano', state: 'TX', zip: '75023', lat: 33.0812, lng: -96.7521 }, + estimatedDuration: 45, status: 'at_risk', arrivedMinsAgo: 47, assignedRepId: 'REP-02', + }, + { + id: 'LD-1006', source: 'website_form', leadType: 'insurance_claim', urgency: 'high', + customer: { name: 'Lisa Thompson', phone: '(972) 555-1006' }, + property: { address: '7654 Preston Rd', city: 'Plano', state: 'TX', zip: '75024', lat: 33.0934, lng: -96.8002 }, + estimatedDuration: 60, status: 'unassigned', arrivedMinsAgo: 5, assignedRepId: null, + }, + { + id: 'LD-1007', source: 'canvassing', leadType: 'retail_estimate', urgency: 'standard', + customer: { name: 'David Park', phone: '(972) 555-1007' }, + property: { address: '2200 Alma Dr', city: 'Plano', state: 'TX', zip: '75075', lat: 33.0516, lng: -96.7289 }, + estimatedDuration: 30, status: 'confirmed', arrivedMinsAgo: 62, assignedRepId: 'REP-04', + }, + { + id: 'LD-1008', source: 'referral', leadType: 'roof_inspection', urgency: 'standard', + customer: { name: 'Angela Moore', phone: '(972) 555-1008' }, + property: { address: '555 Legacy Dr', city: 'Plano', state: 'TX', zip: '75023', lat: 33.0744, lng: -96.7612 }, + estimatedDuration: 45, status: 'unassigned', arrivedMinsAgo: 28, assignedRepId: null, + }, + { + id: 'LD-1009', source: 'call_center', leadType: 'emergency_tarp', urgency: 'emergency', + customer: { name: 'Carlos Rivera', phone: '(972) 555-1009' }, + property: { address: '3301 Avenue K', city: 'Plano', state: 'TX', zip: '75074', lat: 33.0198, lng: -96.6989 }, + estimatedDuration: 90, status: 'unassigned', arrivedMinsAgo: 3, assignedRepId: null, + }, + { + id: 'LD-1010', source: 'website_form', leadType: 'retail_estimate', urgency: 'standard', + customer: { name: 'Patricia Davis', phone: '(972) 555-1010' }, + property: { address: '6711 W Spring Creek Pkwy', city: 'Plano', state: 'TX', zip: '75024', lat: 33.0888, lng: -96.7756 }, + estimatedDuration: 30, status: 'at_risk', arrivedMinsAgo: 55, assignedRepId: 'REP-05', + }, + { + id: 'LD-1011', source: 'canvassing', leadType: 'insurance_claim', urgency: 'high', + customer: { name: 'Michael Anderson', phone: '(972) 555-1011' }, + property: { address: '1800 Jupiter Rd', city: 'Plano', state: 'TX', zip: '75074', lat: 33.0267, lng: -96.7102 }, + estimatedDuration: 60, status: 'unassigned', arrivedMinsAgo: 11, assignedRepId: null, + }, + { + id: 'LD-1012', source: 'referral', leadType: 'roof_inspection', urgency: 'standard', + customer: { name: 'Jennifer White', phone: '(972) 555-1012' }, + property: { address: '4400 W Parker Rd', city: 'Plano', state: 'TX', zip: '75093', lat: 33.0612, lng: -96.8145 }, + estimatedDuration: 45, status: 'confirmed', arrivedMinsAgo: 72, assignedRepId: 'REP-01', + }, +]; + +// Pre-baked AI recommendations keyed by lead ID — no algorithm needed, just convincing data. +// Each rep entry: repId, score, slotStart/End, factors (0-100 each), reasons[], stormReasons[], routePath[[lat,lng]] +export const DISPATCH_RECOMMENDATIONS = { + 'LD-1001': [ + { + repId: 'REP-04', score: 91, slotStart: '1:30 PM', slotEnd: '2:30 PM', + factors: { proximity: 88, driveTime: 90, calendarFit: 95, weather: 90, skillMatch: 100 }, + reasons: ['Closest eligible rep — 2.1 mi', '12 min drive time estimated', 'Insurance claim certified', 'Buffer preserved before next appointment'], + stormReasons: ['Lightning risk before 2 PM — slot pushed to 1:30 PM', 'Rep carries emergency tarp kit'], + routePath: [[33.0516, -96.7512], [33.0612, -96.7484], [33.0708, -96.7456]], + }, + { + repId: 'REP-02', score: 79, slotStart: '3:00 PM', slotEnd: '4:00 PM', + factors: { proximity: 72, driveTime: 75, calendarFit: 82, weather: 80, skillMatch: 85 }, + reasons: ['Available at 3 PM', '18 min drive — moderate traffic', 'Roof inspection certified'], + stormReasons: ['Afternoon slot carries weather risk — monitor closely'], + routePath: [[33.0812, -96.7234], [33.0760, -96.7345], [33.0708, -96.7456]], + }, + { + repId: 'REP-01', score: 71, slotStart: '4:30 PM', slotEnd: '5:30 PM', + factors: { proximity: 62, driveTime: 60, calendarFit: 78, weather: 70, skillMatch: 100 }, + reasons: ['Available later in the day', '24 min drive from current location', 'High performer — score 94'], + stormReasons: ['Late afternoon — weather conditions improving'], + routePath: [[33.0612, -96.7891], [33.0660, -96.7674], [33.0708, -96.7456]], + }, + ], + 'LD-1002': [ + { + repId: 'REP-01', score: 88, slotStart: '2:00 PM', slotEnd: '2:45 PM', + factors: { proximity: 92, driveTime: 88, calendarFit: 85, weather: 88, skillMatch: 100 }, + reasons: ['Closest rep — 1.4 mi from lead location', '8 min drive time', 'Buffer maintained after current job'], + stormReasons: ['Weather clear until 3 PM — slot timing is safe', 'Rep familiar with West Plano territory'], + routePath: [[33.0612, -96.7891], [33.0632, -96.7956], [33.0652, -96.8021]], + }, + { + repId: 'REP-04', score: 74, slotStart: '3:30 PM', slotEnd: '4:15 PM', + factors: { proximity: 70, driveTime: 68, calendarFit: 85, weather: 72, skillMatch: 100 }, + reasons: ['Available at 3:30 PM', '22 min drive from current position', 'Insurance claim certified'], + stormReasons: ['Afternoon slot — weather risk moderate after 3 PM'], + routePath: [[33.0516, -96.7512], [33.0584, -96.7767], [33.0652, -96.8021]], + }, + ], + 'LD-1003': [ + { + repId: 'REP-01', score: 87, slotStart: '11:00 AM', slotEnd: '11:45 AM', + factors: { proximity: 85, driveTime: 88, calendarFit: 90, weather: 85, skillMatch: 100 }, + reasons: ['Currently assigned — optimal for continuity', '11 min drive from current stop', 'Insurance claim specialist'], + stormReasons: ['Morning slot — no weather concerns'], + routePath: [[33.0612, -96.7891], [33.0517, -96.7515], [33.0421, -96.7138]], + }, + { + repId: 'REP-05', score: 67, slotStart: '1:00 PM', slotEnd: '1:45 PM', + factors: { proximity: 65, driveTime: 62, calendarFit: 72, weather: 75, skillMatch: 60 }, + reasons: ['Available afternoon slot', '19 min drive', 'Less specialized — retail estimate primary skill'], + stormReasons: ['Afternoon — monitor weather after 1 PM'], + routePath: [[33.0198, -96.7234], [33.0310, -96.7186], [33.0421, -96.7138]], + }, + ], + 'LD-1004': [ + { + repId: 'REP-03', score: 95, slotStart: '10:30 AM', slotEnd: '11:00 AM', + factors: { proximity: 98, driveTime: 97, calendarFit: 88, weather: 95, skillMatch: 100 }, + reasons: ['0.6 mi away — optimal proximity', '4 min drive time', 'Retail estimate specialist'], + stormReasons: ['Morning slot — conditions favorable'], + routePath: [[33.0389, -96.7401], [33.0389, -96.7368], [33.0389, -96.7334]], + }, + { + repId: 'REP-05', score: 73, slotStart: '2:00 PM', slotEnd: '2:30 PM', + factors: { proximity: 70, driveTime: 68, calendarFit: 80, weather: 72, skillMatch: 80 }, + reasons: ['Available afternoon', '16 min drive', 'Retail estimate capable'], + stormReasons: ['Afternoon slot — watch post-2 PM weather'], + routePath: [[33.0198, -96.7234], [33.0294, -96.7284], [33.0389, -96.7334]], + }, + ], + 'LD-1005': [ + { + repId: 'REP-02', score: 94, slotStart: '12:00 PM', slotEnd: '12:45 PM', + factors: { proximity: 97, driveTime: 96, calendarFit: 90, weather: 92, skillMatch: 95 }, + reasons: ['0.3 mi from lead — walking distance', '3 min drive time', 'Roof inspection specialist', 'Low daily load — 2 of 6 slots used'], + stormReasons: ['Midday window — weather acceptable', 'Rep carries safety equipment'], + routePath: [[33.0812, -96.7234], [33.0812, -96.7378], [33.0812, -96.7521]], + }, + { + repId: 'REP-04', score: 77, slotStart: '2:30 PM', slotEnd: '3:15 PM', + factors: { proximity: 72, driveTime: 70, calendarFit: 88, weather: 78, skillMatch: 90 }, + reasons: ['Available at 2:30 PM', '17 min drive', 'Roof inspection certified'], + stormReasons: ['Afternoon — post-3 PM weather worsening, start punctually'], + routePath: [[33.0516, -96.7512], [33.0664, -96.7517], [33.0812, -96.7521]], + }, + ], + 'LD-1006': [ + { + repId: 'REP-01', score: 83, slotStart: '3:00 PM', slotEnd: '4:00 PM', + factors: { proximity: 80, driveTime: 78, calendarFit: 85, weather: 82, skillMatch: 100 }, + reasons: ['Best available slot — 3 PM', '14 min drive from current route', 'Insurance claim specialist'], + stormReasons: ['Wind advisory active — exterior inspection may need delay', 'Insurance claim can proceed indoors'], + routePath: [[33.0612, -96.7891], [33.0773, -96.7947], [33.0934, -96.8002]], + }, + { + repId: 'REP-02', score: 69, slotStart: '4:30 PM', slotEnd: '5:30 PM', + factors: { proximity: 65, driveTime: 62, calendarFit: 75, weather: 68, skillMatch: 85 }, + reasons: ['Late afternoon availability', '26 min drive', 'Roof inspection skilled'], + stormReasons: ['Late afternoon — conditions deteriorating, not recommended'], + routePath: [[33.0812, -96.7234], [33.0873, -96.7618], [33.0934, -96.8002]], + }, + ], + 'LD-1007': [ + { + repId: 'REP-04', score: 87, slotStart: '11:30 AM', slotEnd: '12:00 PM', + factors: { proximity: 90, driveTime: 88, calendarFit: 92, weather: 90, skillMatch: 80 }, + reasons: ['1.5 mi away — close proximity', '9 min drive time', 'Low daily load', 'Buffer before afternoon appointments'], + stormReasons: ['Morning window — safe conditions'], + routePath: [[33.0516, -96.7512], [33.0516, -96.7401], [33.0516, -96.7289]], + }, + { + repId: 'REP-03', score: 78, slotStart: '1:30 PM', slotEnd: '2:00 PM', + factors: { proximity: 78, driveTime: 75, calendarFit: 70, weather: 82, skillMatch: 100 }, + reasons: ['Retail estimate specialist', '12 min drive', 'Near current work site'], + stormReasons: ['Early afternoon — conditions acceptable'], + routePath: [[33.0389, -96.7401], [33.0453, -96.7345], [33.0516, -96.7289]], + }, + ], + 'LD-1008': [ + { + repId: 'REP-04', score: 86, slotStart: '2:00 PM', slotEnd: '2:45 PM', + factors: { proximity: 84, driveTime: 85, calendarFit: 90, weather: 85, skillMatch: 95 }, + reasons: ['2.7 mi from lead — manageable drive', '15 min estimated travel', 'Roof inspection certified', 'Open afternoon slot'], + stormReasons: ['Weather acceptable until 3:30 PM', 'Book for 2 PM to maximize safe window'], + routePath: [[33.0516, -96.7512], [33.0630, -96.7562], [33.0744, -96.7612]], + }, + { + repId: 'REP-02', score: 75, slotStart: '3:30 PM', slotEnd: '4:15 PM', + factors: { proximity: 72, driveTime: 70, calendarFit: 82, weather: 74, skillMatch: 90 }, + reasons: ['Available at 3:30 PM', '19 min drive', 'Roof inspection specialist'], + stormReasons: ['Late afternoon — weather risk elevated after 3 PM'], + routePath: [[33.0812, -96.7234], [33.0778, -96.7423], [33.0744, -96.7612]], + }, + { + repId: 'REP-01', score: 68, slotStart: '5:00 PM', slotEnd: '5:45 PM', + factors: { proximity: 62, driveTime: 58, calendarFit: 70, weather: 68, skillMatch: 80 }, + reasons: ['End of day slot only', '22 min drive', 'High performer — score 94'], + stormReasons: ['Evening — conditions improving but daylight limited'], + routePath: [[33.0612, -96.7891], [33.0678, -96.7752], [33.0744, -96.7612]], + }, + ], + 'LD-1009': [ + { + repId: 'REP-05', score: 97, slotStart: '11:00 AM', slotEnd: '12:30 PM', + factors: { proximity: 99, driveTime: 98, calendarFit: 95, weather: 95, skillMatch: 90 }, + reasons: ['0.8 mi away — nearest rep on site', '5 min drive time', 'Emergency certified', 'PRIORITY: emergency tarp needed'], + stormReasons: ['EMERGENCY — dispatch immediately', 'Rep stocked with emergency tarp materials'], + routePath: [[33.0198, -96.7234], [33.0198, -96.7112], [33.0198, -96.6989]], + }, + { + repId: 'REP-03', score: 71, slotStart: '1:00 PM', slotEnd: '2:30 PM', + factors: { proximity: 65, driveTime: 62, calendarFit: 75, weather: 72, skillMatch: 75 }, + reasons: ['Backup option — 5 appointments today', '21 min drive', 'Roof inspection capable'], + stormReasons: ['Secondary option — use only if REP-05 unavailable'], + routePath: [[33.0389, -96.7401], [33.0294, -96.7195], [33.0198, -96.6989]], + }, + ], + 'LD-1010': [ + { + repId: 'REP-01', score: 84, slotStart: '2:30 PM', slotEnd: '3:00 PM', + factors: { proximity: 82, driveTime: 80, calendarFit: 88, weather: 84, skillMatch: 80 }, + reasons: ['Best proximity match — 2.9 mi', '16 min drive', 'High performer — score 94', 'Manageable daily load'], + stormReasons: ['Afternoon slot — monitor weather', 'Retail estimate can proceed partially indoors'], + routePath: [[33.0612, -96.7891], [33.0750, -96.7824], [33.0888, -96.7756]], + }, + { + repId: 'REP-02', score: 72, slotStart: '4:00 PM', slotEnd: '4:30 PM', + factors: { proximity: 70, driveTime: 68, calendarFit: 78, weather: 70, skillMatch: 80 }, + reasons: ['Available late afternoon', '22 min drive', 'Roof inspection certified'], + stormReasons: ['4 PM slot — weather conditions deteriorating, not ideal'], + routePath: [[33.0812, -96.7234], [33.0850, -96.7495], [33.0888, -96.7756]], + }, + ], + 'LD-1011': [ + { + repId: 'REP-05', score: 89, slotStart: '12:30 PM', slotEnd: '1:30 PM', + factors: { proximity: 90, driveTime: 88, calendarFit: 85, weather: 88, skillMatch: 90 }, + reasons: ['Closest rep — 1.6 mi', '9 min drive time', 'Insurance claim proficient', 'Midday slot timing is optimal'], + stormReasons: ['Midday window — acceptable conditions', 'Insurance claim can be initiated outdoors then moved inside'], + routePath: [[33.0198, -96.7234], [33.0233, -96.7168], [33.0267, -96.7102]], + }, + { + repId: 'REP-03', score: 76, slotStart: '2:00 PM', slotEnd: '3:00 PM', + factors: { proximity: 74, driveTime: 70, calendarFit: 78, weather: 76, skillMatch: 80 }, + reasons: ['Available at 2 PM', '18 min drive', 'Retail estimate skilled'], + stormReasons: ['Afternoon slot — conditions marginal after 2:30 PM'], + routePath: [[33.0389, -96.7401], [33.0328, -96.7252], [33.0267, -96.7102]], + }, + ], + 'LD-1012': [ + { + repId: 'REP-01', score: 92, slotStart: '10:00 AM', slotEnd: '10:45 AM', + factors: { proximity: 94, driveTime: 92, calendarFit: 88, weather: 92, skillMatch: 95 }, + reasons: ['Currently assigned — optimal', '1.7 mi from current position', 'Morning slot — ideal weather window', 'Insurance + retail estimate certified'], + stormReasons: ['Morning slot — safe window before any afternoon weather'], + routePath: [[33.0612, -96.7891], [33.0612, -96.8018], [33.0612, -96.8145]], + }, + { + repId: 'REP-04', score: 70, slotStart: '1:00 PM', slotEnd: '1:45 PM', + factors: { proximity: 65, driveTime: 62, calendarFit: 82, weather: 70, skillMatch: 85 }, + reasons: ['Afternoon availability', '24 min drive from current location', 'Roof inspection certified'], + stormReasons: ['Afternoon — conditions acceptable but watch post-2 PM'], + routePath: [[33.0516, -96.7512], [33.0564, -96.7829], [33.0612, -96.8145]], + }, + ], +}; + // --- CONTEXT SETUP --- const MockStoreContext = createContext(); @@ -2739,6 +3072,7 @@ export const MockStoreProvider = ({ children }) => { const [orders, setOrders] = useState(MOCK_ORDERS); const [vendorInvoices, setVendorInvoices] = useState(MOCK_VENDOR_INVOICES); const [leads, setLeads] = useState([]); + const [dispatchLeads, setDispatchLeads] = useState(DISPATCH_LEADS_INITIAL); // Initialize properties once useEffect(() => { @@ -2839,12 +3173,19 @@ export const MockStoreProvider = ({ children }) => { return newLead; }, + // LynkDispatch + dispatchLeads, + assignDispatchLead: (leadId, repId) => { + setDispatchLeads(prev => prev.map(l => + l.id === leadId ? { ...l, assignedRepId: repId, status: 'confirmed' } : l + )); + }, + updatePropertyStatus, addMeeting, updateUser: (updatedUser) => { setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u)); }, - // Export new actions assignAgent, markLeadContacted, sendReminder diff --git a/src/pages/LynkDispatchPage.jsx b/src/pages/LynkDispatchPage.jsx new file mode 100644 index 0000000..0be6e3f --- /dev/null +++ b/src/pages/LynkDispatchPage.jsx @@ -0,0 +1,301 @@ +import React, { useState } from 'react'; +import { Zap, CloudLightning, Radio, Inbox, CheckCircle, Clock, AlertTriangle, Bot } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useTheme } from '../context/ThemeContext'; +import { useMockStore, DISPATCH_REPS, DISPATCH_RECOMMENDATIONS } from '../data/mockStore'; + +// --------------------------------------------------------------------------- +// Panel — themed container used by all 3 side panels +// --------------------------------------------------------------------------- +const Panel = ({ children, className = '' }) => ( +
+ {children} +
+); + +const PanelHeader = ({ title, subtitle, accent, right }) => ( +
+
+

{title}

+ {subtitle &&

{subtitle}

} +
+ {right &&
{right}
} +
+); + +// --------------------------------------------------------------------------- +// KPI Card — bottom bar metric card +// --------------------------------------------------------------------------- +const KpiCard = ({ label, value, icon: Icon, accent, stormMode, pulse }) => ( +
+
+ +
+
+

{label}

+

{value}

+
+ {stormMode && pulse && ( + + )} +
+); + +// --------------------------------------------------------------------------- +// Main Page +// --------------------------------------------------------------------------- +const LynkDispatchPage = () => { + const { theme } = useTheme(); + const { dispatchLeads } = useMockStore(); + + const [selectedLead, setSelectedLead] = useState(null); + const [stormMode, setStormMode] = useState(false); + const [isProcessing, setIsProcessing] = useState(false); + + const accent = stormMode ? '#F59E0B' : '#3B82F6'; + const unassigned = dispatchLeads.filter(l => l.status === 'unassigned').length; + + return ( +
+ + {/* Storm Banner */} + + {stormMode && ( + +
+ + Storm Mode Active — Exterior Inspections Restricted — Route Planning Adjusted + +
+
+ )} +
+ + {/* ── Header ─────────────────────────────────────────────────────── */} +
+ {/* Branding */} +
+
+ +
+
+

+ LynkDispatch +

+

AI Dispatch Engine

+
+
+ + {/* Right controls */} +
+ {/* LIVE badge */} +
+ + Live +
+ + {/* Storm Mode Toggle */} + +
+
+ + {/* ── 3-Panel Grid ───────────────────────────────────────────────── */} + {/* Desktop: [320px fixed] [flex-1 map] [340px fixed] */} + {/* Mobile: single column, scrollable */} +
+ + {/* LEFT — Lead Queue */} + + + {dispatchLeads.length} + + } + /> + {/* Placeholder — Phase 2 builds this */} +
+
+ +
+
+

{dispatchLeads.length} leads loaded

+

Lead queue renders in Phase 2

+
+ {/* Quick stat pills */} +
+ {[ + { label: 'Unassigned', count: dispatchLeads.filter(l => l.status === 'unassigned').length, color: '#EF4444' }, + { label: 'At Risk', count: dispatchLeads.filter(l => l.status === 'at_risk').length, color: '#F59E0B' }, + { label: 'Confirmed', count: dispatchLeads.filter(l => l.status === 'confirmed').length, color: '#10B981' }, + ].map(s => ( + + {s.count} {s.label} + + ))} +
+
+
+ + {/* CENTER — Map */} + + + + {DISPATCH_REPS.length} reps active +
+ } + /> + {/* Placeholder — Phase 4 builds Leaflet map here */} +
+
+ +
+
+

Interactive Map

+

Leaflet map with rep markers & route polylines — Phase 4

+
+ {/* Rep status pills */} +
+ {[ + { label: 'Available', count: DISPATCH_REPS.filter(r => r.status === 'available').length, color: '#10B981' }, + { label: 'En Route', count: DISPATCH_REPS.filter(r => r.status === 'en_route').length, color: '#3B82F6' }, + { label: 'Busy', count: DISPATCH_REPS.filter(r => r.status === 'busy').length, color: '#F59E0B' }, + ].map(s => ( + + {s.count} {s.label} + + ))} +
+
+ + + {/* RIGHT — AI Recommendation Drawer */} + + + +
+ } + /> + {/* Empty state — Phase 3 builds this */} +
+ + + +
+

+ {selectedLead ? 'Processing...' : 'No lead selected'} +

+

+ Click a lead in the queue to see AI-scored rep recommendations +

+
+

+ Recommendation engine — Phase 3 +

+
+ + + + {/* ── KPI Bar ────────────────────────────────────────────────────── */} +
+ + + + +
+ + ); +}; + +export default LynkDispatchPage;