import React, { useState, useEffect, useMemo } from 'react';
import { Search, MapPin, Phone, Mail, Plus, Edit2, Trash2, X, Navigation, Clock, User, AlertCircle, Filter, ArrowUpDown, Globe, Car, Building2, ExternalLink, Upload, Download, Map as MapIcon } from 'lucide-react';
// --- Initial Data ---
const INITIAL_INSTRUCTORS = [
{ id: 1, name: "Saad Mohammed", city: "Highland", state: "Indiana", phone: "(219) 318-6615", email: "saadmohammed.pt@gmail.com", company: "Giving Hearts", website: "https://www.givingheartscpr.com/", multiArea: false, lat: 41.553, lon: -87.452 },
{ id: 2, name: "Yessenia Orta", city: "Munster", state: "Indiana", phone: "(219) 455-9070", email: "yessenia_24@yahoo.com", company: "Giving Hearts", website: "https://www.givingheartscpr.com", multiArea: false, lat: 41.564, lon: -87.512 },
{ id: 3, name: "AYO WILLIAMS", city: "Burtonsville", state: "Maryland", phone: "(240) 751-6997", email: "awam2005@yahoo.com", company: "Independent", website: "", multiArea: false, lat: 38.99, lon: -77.031 },
{ id: 4, name: "Sheila Davis", city: "Arlington", state: "Washington", phone: "(425) 931-4537", email: "sheila@bee-safe.com", company: "Bee-Safe", website: "https://www.bee-safe.com", multiArea: true, lat: 48.16, lon: -122.14 },
{ id: 5, name: "Ted Paul", city: "Delray Beach", state: "Florida", phone: "(561) 926-3143", email: "medtranplus@gmail.com", company: "Med Tran Plus", website: "https://medtranplus.com", multiArea: false, lat: 26.461, lon: -80.072 },
{ id: 6, name: "Evelyn Piraino", city: "Compton", state: "California", phone: "(562) 583-8500", email: "evelyncarlos21@yahoo.com", company: "Independent", website: "", multiArea: false, lat: 33.895, lon: -118.22 },
{ id: 7, name: "Oishi Harris", city: "Batavia", state: "New York", phone: "(585) 201-1135", email: "oishi.moni66@icloud.com", company: "CPR 2 YOU", website: "", multiArea: false, lat: 42.998, lon: -78.187 },
{ id: 8, name: "Tuba Kazan", city: "Chula Vista", state: "California", phone: "(619) 942-8274", email: "sandiegoheartsaver@yahoo.com", company: "San Diego Medical College", website: "", multiArea: false, lat: 32.64, lon: -117.084 },
{ id: 9, name: "Bethany Moreland", city: "Kokomo", state: "Indiana", phone: "(765) 431-6575", email: "bmoreland@rosseducation.edu", company: "CPR 2 YOU", website: "", multiArea: false, lat: 40.486, lon: -86.133 },
{ id: 10, name: "Serrena Gregg", city: "Dunkirk", state: "Indiana", phone: "(705) 716-0785", email: "sgregg@rosseducation.edu", company: "CPR 2 YOU", website: "cpr2you.org", multiArea: false, lat: 40.373, lon: -85.208 },
{ id: 11, name: "Kara Osada-D'Avelia", city: "Kailua Kona", state: "Hawaii", phone: "(808) 326-7836", email: "heart@islandcpr.com", company: "Island CPR", website: "https://www.islandcpr.com/", multiArea: true, lat: 19.64, lon: -156.996 },
{ id: 12, name: "Cody Willame", city: "Carrolltown", state: "Pennsylvania", phone: "(814) 418-2973", email: "jcaas64@gmail.com", company: "Independent", website: "", multiArea: false, lat: 40.606, lon: -78.708 },
{ id: 13, name: "Sterling Castillo", city: "Fairfield", state: "California", phone: "(888) 400-8470", email: "sc@cprdudeusa.com", company: "CPDUDEUSA", website: "https://cprdudeusa.com", multiArea: false, lat: 38.249, lon: -122.04 },
{ id: 14, name: "Taha Quareshi", city: "East Chicago", state: "Indiana", phone: "219-314-5311", email: "givingheartscpr@gmail.com", company: "Giving Hearts", website: "https://www.givingheartscpr.com", multiArea: false, lat: 41.639, lon: -87.454 },
{ id: 15, name: "Doug Levy", city: "Fredonia", state: "New York", phone: "267-994-8065", email: "dlevy@computers-cpu.com", company: "Independent", website: "", multiArea: false, lat: 42.44, lon: -79.331 },
{ id: 16, name: "Lucy Loomis", city: "Liverpool", state: "New York", phone: "315-806-1413", email: "lucloomis@aol.com", company: "Independent", website: "", multiArea: false, lat: 43.106, lon: -76.217 },
{ id: 17, name: "David Yelovich", city: "Ralston", state: "Nebraska", phone: "402-547-9760", email: "david@lifesaving101.com", company: "FireEagle CPR", website: "https://lifesaving101.com", multiArea: false, lat: 41.2, lon: -96.04 },
{ id: 18, name: "Dana Siccaas", city: "Orlando", state: "Florida", phone: "407-308-0139", email: "heartcoremedicaltraining@gmail.com", company: "Heartcore CPR", website: "heartcoremedicaltraining.com", multiArea: false, lat: 28.538, lon: -81.379 },
{ id: 19, name: "Shakara", city: "Pittsburgh", state: "Pennsylvania", phone: "412-212-6840", email: "shakara@handsoverheartscpr.com", company: "Hands Over Hearts", website: "https://www.handsoverheartscpr.com/", multiArea: false, lat: 40.44, lon: -79.995 },
{ id: 20, name: "Anthony Derubbis", city: "Pittsburgh", state: "Pennsylvania", phone: "412-254-6665", email: "Anthony@specialtasksgroupsecurity.com", company: "Special Task Group", website: "https://specialtasksgroupsecurity.com", multiArea: false, lat: 40.44, lon: -79.995 },
{ id: 21, name: "Shanta Smith", city: "Omaha", state: "Nebraska", phone: "531-225-9262", email: "sdsroberts@gmail.com", company: "Independent", website: "", multiArea: false, lat: 41.25, lon: -95.99 },
{ id: 22, name: "Toby Schapiro", city: "Buffalo", state: "New York", phone: "585-480-0424", email: "onxdynamics@gmail.com", company: "OnXDynamics", website: "https://www.onxdynamics.com", multiArea: true, lat: 42.886, lon: -78.878 },
{ id: 23, name: "Uma Yoganathan", city: "Utica", state: "New York", phone: "607-207-2710", email: "cprtrainingny@outlook.com", company: "Independent", website: "", multiArea: false, lat: 43.1, lon: -75.232 },
{ id: 24, name: "Amy Whers", city: "Binghamton", state: "New York", phone: "607-761-1687", email: "amyupny76@gmail.com", company: "A+ Plus Care", website: "https://www.facebook.com/profile.php", multiArea: false, lat: 42.098, lon: -75.917 },
{ id: 25, name: "Karla", city: "Allentown", state: "Pennsylvania", phone: "610-351-2427", email: "APLUSSAFETY@AOL.COM", company: "Aplussaftey", website: "", multiArea: false, lat: 40.6, lon: -75.47 },
{ id: 26, name: "John Shoen", city: "Wayne", state: "Pennsylvania", phone: "610-745-2274", email: "john@shoensafety.com", company: "Shoen Safety", website: "", multiArea: false, lat: 40.044, lon: -75.387 },
{ id: 27, name: "Hannah Miterko", city: "Rochester", state: "New York", phone: "716-342-5888", email: "hannah.miterko21@gmail.com", company: "CPR 2 YOU", website: "", multiArea: false, lat: 43.156, lon: -77.608 },
{ id: 28, name: "Joshua VanRemmen", city: "Buffalo", state: "New York", phone: "716-997-4635", email: "rechargedplustraining@gmail.com", company: "Potential CPR 2 YOU", website: "", multiArea: false, lat: 42.88, lon: -78.87 },
{ id: 29, name: "Charles Smith", city: "Mamaroneck", state: "New York", phone: "718-795-7852", email: "breathecprcourses@gmail.com", company: "BreatheCPR", website: "https://www.breathecpr.org", multiArea: false, lat: 40.948, lon: -73.732 },
{ id: 30, name: "Tasha Beck", city: "Pittsburgh", state: "Pennsylvania", phone: "814-279-8961", email: "tasha.rxwound@gmail.com", company: "Potential CPR 2 YOU", website: "", multiArea: false, lat: 40.44, lon: -79.995 },
{ id: 31, name: "Makenzie Stewart", city: "Philadelphia", state: "Pennsylvania", phone: "866-603-3430", email: "mstewart@tristatetraining.com", company: "Tristate Training", website: "tristatetraining.com", multiArea: false, lat: 39.952, lon: -75.165 },
{ id: 32, name: "Alex Balish", city: "Piscataway", state: "New Jersey", phone: "908.443.1277", email: "Info@care1stcpr.com", company: "Care1st CPR", website: "care1stcpr.com", multiArea: false, lat: 40.55, lon: -74.46 },
{ id: 33, name: "JL Henry", city: "Albany", state: "New York", phone: "", email: "", company: "", website: "", multiArea: false, lat: 42.652, lon: -73.756 }
];
// --- Utilities ---
const geocodeAddress = async (query) => {
try {
const nomResponse = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1`);
if (nomResponse.ok) {
const data = await nomResponse.json();
if (data && data.length > 0) {
return { lat: parseFloat(data[0].lat), lon: parseFloat(data[0].lon) };
}
}
} catch (e) {
console.warn("Primary geocoding service failed, attempting backup...", e);
}
try {
const photonResponse = await fetch(`https://photon.komoot.io/api/?q=${encodeURIComponent(query)}&limit=1`);
if (photonResponse.ok) {
const data = await photonResponse.json();
if (data.features && data.features.length > 0) {
const [lon, lat] = data.features[0].geometry.coordinates;
return { lat, lon };
}
}
} catch (e) {
console.error("Backup geocoding service failed", e);
}
return null;
};
const calculateDistance = (lat1, lon1, lat2, lon2) => {
if (!lat1 || !lon1 || !lat2 || !lon2) return 9999;
const R = 3959;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
};
const estimateDriveTime = (distanceMiles) => {
const roadFactor = 1.25;
const averageSpeedMph = 55;
const hours = (distanceMiles * roadFactor) / averageSpeedMph;
const h = Math.floor(hours);
const m = Math.round((hours - h) * 60);
return { hours: h, minutes: m, totalHours: hours };
};
// --- Map Data ---
const US_STATE_PATHS = {
"Alabama": "M576.7,469.7l-4.2,46.1l-24.1-1.3l-13-26l-20.1-66.9l43-3l12.4,14.3l3.3,27.1L576.7,469.7z",
"Alaska": "M117.4,591.9l-19.4,26.6l-11.8-6.1l1.5-6.5l-6.5-12.8l2-7.8l-1.5-5.9l8.6-1.5l14.9,2.4l11.5,4.3L117.4,591.9z M267.8,604.6 l-6.9-1.9l-11.5-12.2l-3.3-13.4l-14.7-6.5l-11.1-15.3l-10.7-3.3l-13.8,9.4l-30.8-21.6l-13.4-1.7l-26.6-28.7l-15.5-5.9l-3.3-15.3 l10.1-15.9l12.1-7.8l26.4,1l15.1-4l31.4,15.1l40.4,2.3l6.5,14.5l-1.9,13.2l20.5,35.8l-1.5,22.2l-4.4,5.2L267.8,604.6z",
"Arizona": "M195.9,400.9l-5.7-88.4l75.4-15.1l19.5,115.6l-85.3,8.6L195.9,400.9z",
"Arkansas": "M491.7,404l-2.7,49.2l-64.3,2.5l-1-29.3l-6.5-7.8l8.2-38l12.1-4.8l53.2,0.8L491.7,404z",
"California": "M60.2,216.5l55.9,3.6l17.2,56.6l64.3,106.6l-21.4,13.2l-68.9-19.1l-11.1-17.6l-29.3-33.1L60.2,216.5z",
"Colorado": "M266.1,289.8l93-5.2l7.1,70.8l-94.7,6.3L266.1,289.8z",
"Connecticut": "M718.5,233.5l1.3,16.6l-21.6,4.6l-0.7-16.1L718.5,233.5z",
"Delaware": "M688.1,299l0.8,16.5l-8.9-2.3l1.8-19.1L688.1,299z",
"Florida": "M596.2,510.9l22.6,3.6l37.1,51.8l-4.8,9.8l-18.4,1.9l-16.3-26.8l-37.9-16.1l-1.9-35.6l23.7-2L596.2,510.9z",
"Georgia": "M609.6,427.6l14.7,21.6l-4.4,47.8l-43.2-3.4l-1.1-26.2l-3.3-25.6l-12.8-14l35.8-9.4L609.6,427.6z",
"Hawaii": "M243.2,639.1l-4.8,2.7l-5.2-6.5l3.4-6.1l6.9,2.3L243.2,639.1z M265.6,651.9l-5.9,2.1l-6.1-9.6l5.2-3.1L265.6,651.9z M280.9,657.3l-9.2,8l-7.3-6.1l9.4-11.5L280.9,657.3z M301.9,668l-8.2,13.6l-15.1-6.9l8.6-14.7L301.9,668z",
"Idaho": "M174.5,93.6l15.9,1.7l6.7,60.8l20.5,9l-11.5,67.7l-49.7-8.2L156.9,132l16.1-23.9L174.5,93.6z",
"Illinois": "M504.3,275l24.7,0.6l5.4,85l-19.3,20.3l-28.9-19.9l-6.9-39.2l12.8-5.9L504.3,275z",
"Indiana": "M533.8,279.8l24.3,1.3l-2.1,64.1l-10.7,8.2l-15.7-11.9l-3.4-16.8L533.8,279.8z",
"Iowa": "M443.9,249.4l49.3,2.5l5.2,15.7l-3.3,31.4l-11.9,4l-48.4-3.1L443.9,249.4z",
"Kansas": "M371,317.9l75-2.7l1.7,44.6l-78.4,1.9L371,317.9z",
"Kentucky": "M566.9,343.8l25.1,10.9l-9.4,22l-77.9-5.4l5.4-15.7l17.2-12.8L566.9,343.8z",
"Louisiana": "M460.6,464.3l3.6,18l24.5,5.2l20.3-6.5l3.3-25.8l-12.4-1.3l-2.1-43.4l-43.8,0L460.6,464.3z",
"Maine": "M727.5,130.6l23.4,18l-1.9,35.8l-23.7,11.3l-5.7-20.7l-9-4.8l7.5-31.2L727.5,130.6z",
"Maryland": "M662.3,285.3l30.8,3.8l-5.2,20.1l-24.9-3.4l-3.4-6.3l-13.8,3.8l-1.5-7.3l12.8-6.1L662.3,285.3z",
"Massachusetts": "M721.2,217.4l19.5,1.7l-0.4,15.5l-21.4-1.9L721.2,217.4z",
"Michigan": "M511.4,204.6l25.3,13.8l-6.3,20.9l-18.4,2.9l-11.7-18L511.4,204.6z M539.3,243.3l26.4,17.2l-5.2,33.1l-25.3-2.3L539.3,243.3z",
"Minnesota": "M427.8,136.6l49.3,7.1l-10.3,47.8l8.2,16.5l-5.7,29.9l-51.4-2.5L427.8,136.6z",
"Mississippi": "M505.4,411.3l20.9-1.3l-2.5,77.7l-15.3-0.8l-7.3,10.7l-5.6-2l3.3-30.7L505.4,411.3z",
"Missouri": "M452.9,301.7l46.2,0.8l5.2,36.4l-2.5,23.4l-23.9,1.7l-4.4,39.7l-53.5-2.7l0.2-57.7L452.9,301.7z",
"Montana": "M190.2,95.7l102.6,9l-5,59.8l-99.3,0.4l-6.5-32.8L190.2,95.7z",
"Nebraska": "M328.6,260.9l80.5,2.7l5.6,26.4l-8.4,31.4l-101.8,0.4l-0.4-38.2L328.6,260.9z",
"Nevada": "M124.9,223l64.3-1.1l-11.7,112.9l-40.4-18.8L124.9,223z",
"New Hampshire": "M710.9,183.1l9.4,3.1l-2.7,27l-9.2,2.3L710.9,183.1z",
"New Jersey": "M693.3,261.3l7.8,4.6l-2.7,26.2l-8.6-8.2L693.3,261.3z",
"New Mexico": "M271.7,358.5l68.7,4l-4.2,93.6l-67.9-4.2L271.7,358.5z",
"New York": "M654.5,200.4l35.8-16.3l6.5,50.8l-4.8,19.3l-12.8-5l-1.3-15.5l-27.4-2.5L654.5,200.4z",
"North Carolina": "M618.4,369.3l49.3-5.4l14,14.5l-10.7,18l-54.7,20.9l-38.3-5.2l7.1-23.4L618.4,369.3z",
"North Dakota": "M336.5,134.1l74.6,5.2l-4.4,48.5l-73.4-1.3L336.5,134.1z",
"Ohio": "M566,275.8l26.2-7.1l11.1,19.9l-12.4,35.6l-32.4-4.8L566,275.8z",
"Oklahoma": "M340.5,363.5l116.2,3.3l-1.1,44.7l-55.9-2.9l-4.8-13.8l-57.9-0.2L340.5,363.5z",
"Oregon": "M75.1,146.4l82.8,7.3l-13.6,67.7l-90.1-12.8L75.1,146.4z",
"Pennsylvania": "M640.7,249.4l46.2-11.7l1.3,31.4l-47.8,7.3L640.7,249.4z",
"Rhode Island": "M725.2,238.1l4.4,0.6l-1.1,6.5l-5.4-1.5L725.2,238.1z",
"South Carolina": "M612.9,394.8l38.1,7.3l-7.3,35.8l-23.7-6.1l-14.7-21.4L612.9,394.8z",
"South Dakota": "M330.9,188.7l73.8,2.1l-4.8,49.2l-77.1-2.9L330.9,188.7z",
"Tennessee": "M519.8,373.9l75-9.8l6.1,23.2l-14.2,1.3l-7.3,12.6l-67.9-4L519.8,373.9z",
"Texas": "M339.4,363.9l0,58.3l-37.1,43.4l27.4,66.4l23.7,2.1l9,32.4l21.6-1.5l10.7-32.8l27.8-14.7l2.1-48.5l-32.8-5l-1.9-45.1l-46.2-0.8L339.4,363.9z",
"Utah": "M206.1,283.7l50.4-2.5l5.2,80l-53.5,1.9L206.1,283.7z",
"Vermont": "M699.4,185.2l9.4,2.3l2.7,30.3l-6.1-0.2L699.4,185.2z",
"Virginia": "M634.6,310.2l12.4-7.1l11.1,27.1l20.7,7.1l-5.6,22l-64.3,6.3l-2.7-18.4L634.6,310.2z",
"Washington": "M92.7,92.5l67.5,7.5l-7.5,43.6l-74-5.9L92.7,92.5z",
"West Virginia": "M610,303.3l6.5-12.8l17.4,13.8l2.9,16.5l-20.9,6.7l-20.1-13L610,303.3z",
"Wisconsin": "M483.2,192.6l23.2,5.7l9.8,28.3l-4.4,23.4l-31.2-1.3L483.2,192.6z",
"Wyoming": "M257.4,167.3l80.1,6.5l-7.3,71.7l-82.6-3.8L257.4,167.3z"
};
// --- State Metadata (Centroids & Abbr) ---
const STATE_META = {
"Alabama": { abbr: "AL", x: 550, y: 460 },
"Alaska": { abbr: "AK", x: 150, y: 600 },
"Arizona": { abbr: "AZ", x: 180, y: 420 },
"Arkansas": { abbr: "AR", x: 460, y: 420 },
"California": { abbr: "CA", x: 60, y: 320 },
"Colorado": { abbr: "CO", x: 290, y: 320 },
"Connecticut": { abbr: "CT", x: 730, y: 240 },
"Delaware": { abbr: "DE", x: 700, y: 300 },
"Florida": { abbr: "FL", x: 640, y: 540 },
"Georgia": { abbr: "GA", x: 600, y: 450 },
"Hawaii": { abbr: "HI", x: 270, y: 650 },
"Idaho": { abbr: "ID", x: 160, y: 150 },
"Illinois": { abbr: "IL", x: 510, y: 300 },
"Indiana": { abbr: "IN", x: 550, y: 300 },
"Iowa": { abbr: "IA", x: 440, y: 260 },
"Kansas": { abbr: "KS", x: 380, y: 340 },
"Kentucky": { abbr: "KY", x: 570, y: 360 },
"Louisiana": { abbr: "LA", x: 460, y: 490 },
"Maine": { abbr: "ME", x: 750, y: 130 },
"Maryland": { abbr: "MD", x: 680, y: 290 },
"Massachusetts": { abbr: "MA", x: 730, y: 215 },
"Michigan": { abbr: "MI", x: 540, y: 220 },
"Minnesota": { abbr: "MN", x: 430, y: 170 },
"Mississippi": { abbr: "MS", x: 510, y: 460 },
"Missouri": { abbr: "MO", x: 450, y: 340 },
"Montana": { abbr: "MT", x: 230, y: 120 },
"Nebraska": { abbr: "NE", x: 350, y: 280 },
"Nevada": { abbr: "NV", x: 110, y: 260 },
"New Hampshire": { abbr: "NH", x: 720, y: 190 },
"New Jersey": { abbr: "NJ", x: 705, y: 270 },
"New Mexico": { abbr: "NM", x: 280, y: 430 },
"New York": { abbr: "NY", x: 690, y: 190 },
"North Carolina": { abbr: "NC", x: 660, y: 380 },
"North Dakota": { abbr: "ND", x: 350, y: 140 },
"Ohio": { abbr: "OH", x: 590, y: 290 },
"Oklahoma": { abbr: "OK", x: 390, y: 400 },
"Oregon": { abbr: "OR", x: 80, y: 180 },
"Pennsylvania": { abbr: "PA", x: 650, y: 260 },
"Rhode Island": { abbr: "RI", x: 735, y: 235 },
"South Carolina": { abbr: "SC", x: 630, y: 410 },
"South Dakota": { abbr: "SD", x: 350, y: 210 },
"Tennessee": { abbr: "TN", x: 550, y: 390 },
"Texas": { abbr: "TX", x: 370, y: 480 },
"Utah": { abbr: "UT", x: 200, y: 310 },
"Vermont": { abbr: "VT", x: 705, y: 180 },
"Virginia": { abbr: "VA", x: 660, y: 330 },
"Washington": { abbr: "WA", x: 110, y: 100 },
"West Virginia": { abbr: "WV", x: 620, y: 310 },
"Wisconsin": { abbr: "WI", x: 490, y: 200 },
"Wyoming": { abbr: "WY", x: 270, y: 210 }
};
// --- Components ---
const CoverageMapModal = ({ instructors, onClose, onSelectState }) => {
// Determine which states have at least one instructor
const coveredStates = useMemo(() => {
const states = new Set();
instructors.forEach(i => {
if (i.state) {
const cleanState = i.state.trim();
states.add(cleanState);
}
});
return states;
}, [instructors]);
return (
);
};
const ImportModal = ({ onImport, onClose }) => {
const [csvText, setCsvText] = useState('');
const [status, setStatus] = useState('');
const handleProcess = async () => {
setStatus('Processing...');
const lines = csvText.split('\n');
const newInstructors = [];
// Very basic CSV parser - assumes Order: Name, City, State, Phone, Email...
for (let line of lines) {
const parts = line.split(',').map(p => p.trim());
if (parts.length < 3) continue;
const name = parts[0];
const city = parts[1];
const state = parts[2];
// Skip header
if (name.toLowerCase() === 'instructor' || name.toLowerCase() === 'name') continue;
if (name && city && state) {
// Try to geocode immediately
const query = `${city}, ${state}`;
const coords = await geocodeAddress(query);
newInstructors.push({
id: Date.now() + Math.random(),
name,
city,
state,
phone: parts[3] || '',
email: parts[4] || '',
company: parts[5] || '', // Assuming column structure from snippets
website: parts[6] || '',
multiArea: false,
lat: coords ? coords.lat : 0,
lon: coords ? coords.lon : 0
});
// Small delay to be nice to the free API
await new Promise(r => setTimeout(r, 800));
}
}
onImport(newInstructors);
setStatus(`Imported ${newInstructors.length} instructors!`);
setTimeout(onClose, 1500);
};
return (
);
};
const InstructorCard = ({ instructor, searchCoords, onEdit, onDelete }) => {
const distance = searchCoords ? calculateDistance(searchCoords.lat, searchCoords.lon, instructor.lat, instructor.lon) : 0;
const driveTime = estimateDriveTime(distance);
const isWithinRange = driveTime.totalHours <= 2.0;
const isFar = driveTime.totalHours > 4.0;
const distDisplay = distance < 10 ? distance.toFixed(1) : Math.round(distance);
return (
);
};
export default function DispatcherTool() {
const [instructors, setInstructors] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const [searchCoords, setSearchCoords] = useState(null);
const [loadingSearch, setLoadingSearch] = useState(false);
const [searchError, setSearchError] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [isImporting, setIsImporting] = useState(false);
const [isMapVisible, setIsMapVisible] = useState(false);
const [editingInstructor, setEditingInstructor] = useState(null);
const [filterState, setFilterState] = useState('All');
const [sortBy, setSortBy] = useState('name');
useEffect(() => {
const saved = localStorage.getItem('myInstructors');
if (saved) {
setInstructors(JSON.parse(saved));
} else {
setInstructors(INITIAL_INSTRUCTORS);
}
}, []);
useEffect(() => {
if (instructors.length > 0) {
localStorage.setItem('myInstructors', JSON.stringify(instructors));
}
}, [instructors]);
const uniqueStates = ['All', ...new Set(instructors.map(i => i.state).filter(Boolean))].sort();
const handleSearch = async (e) => {
e.preventDefault();
if (!searchQuery.trim()) return;
setLoadingSearch(true);
setSearchError('');
setSearchCoords(null);
try {
const coords = await geocodeAddress(searchQuery);
if (coords) {
setSearchCoords({ lat: coords.lat, lon: coords.lon, query: searchQuery });
setSortBy('distance');
} else {
setSearchError('Location not found. Please check the spelling.');
}
} catch (err) {
setSearchError('Unable to connect to map service.');
} finally {
setLoadingSearch(false);
}
};
const handleSaveInstructor = (data) => {
if (editingInstructor) {
setInstructors(instructors.map(i => i.id === editingInstructor.id ? { ...data, id: i.id } : i));
} else {
setInstructors([...instructors, { ...data, id: Date.now() }]);
}
setIsEditing(false);
setEditingInstructor(null);
};
const handleDelete = (id) => {
if (window.confirm('Are you sure you want to remove this instructor?')) {
setInstructors(instructors.filter(i => i.id !== id));
}
};
const handleExport = () => {
const headers = "Name,City,State,Phone,Email,Company,Website,WillingToTravel,Latitude,Longitude\n";
const csvContent = instructors.map(i =>
`"${i.name}","${i.city}","${i.state}","${i.phone}","${i.email}","${i.company}","${i.website}","${i.multiArea}","${i.lat}","${i.lon}"`
).join("\n");
const blob = new Blob([headers + csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'dispatcher_instructors.csv');
document.body.appendChild(link);
link.click();
};
const handleImport = (newInstructors) => {
// Avoid duplicates by name
const existingNames = new Set(instructors.map(i => i.name.toLowerCase()));
const uniqueNew = newInstructors.filter(i => !existingNames.has(i.name.toLowerCase()));
setInstructors([...instructors, ...uniqueNew]);
};
const handleMapStateSelect = (stateName) => {
setFilterState(stateName);
setIsMapVisible(false);
};
const processedInstructors = [...instructors]
.filter(instructor => filterState === 'All' || instructor.state === filterState)
.sort((a, b) => {
if (sortBy === 'distance' && searchCoords) {
const distA = calculateDistance(searchCoords.lat, searchCoords.lon, a.lat, a.lon);
const distB = calculateDistance(searchCoords.lat, searchCoords.lon, b.lat, b.lon);
return distA - distB;
}
if (sortBy === 'state') return a.state.localeCompare(b.state) || a.name.localeCompare(b.name);
return a.name.localeCompare(b.name);
});
const inRangeCount = searchCoords ? processedInstructors.filter(i => estimateDriveTime(calculateDistance(searchCoords.lat, searchCoords.lon, i.lat, i.lon)).totalHours <= 2).length : 0;
return (
{searchCoords && 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>{inRangeCount} within 2 hours}
{isEditing && { setIsEditing(false); setEditingInstructor(null); }} />}
{isImporting && setIsImporting(false)} />}
{isMapVisible && setIsMapVisible(false)} onSelectState={handleMapStateSelect} />}
);
}
Instructor Coverage Map
Active
No Coverage
{/* SVG Map */}
Showing coverage for all 50 US States based on current instructor list.
Click on a blue state to filter the list.
Click on a blue state to filter the list.
Bulk Import
Paste your CSV content here (Name, City, State, Phone, Email, Company, Website). Note: This will attempt to auto-locate coordinates.
{/* Left: Info */}
{instructor.company && (
)}
{instructor.city}, {instructor.state}
{/* Center: Metrics (Only if searching) */}
{searchCoords && (
{driveTime.hours > 0 ? `${driveTime.hours} hr ` : ''}{driveTime.minutes} min
{isWithinRange && (
In Range
)}
)}
{/* Right: Actions */}
);
};
const InstructorForm = ({ initialData, onSave, onCancel }) => {
const [formData, setFormData] = useState(initialData || { name: '', city: '', state: '', phone: '', email: '', company: '', website: '', multiArea: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleChange = (e) => {
const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
setFormData({ ...formData, [e.target.name]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const query = `${formData.city}, ${formData.state}`;
const coords = await geocodeAddress(query);
if (coords) {
onSave({ ...formData, lat: coords.lat, lon: coords.lon });
} else {
setError('Could not find location coordinates. Please check spelling.');
}
} catch (err) {
setError('Error fetching location data. Service may be unavailable.');
} finally {
setLoading(false);
}
};
return (
{instructor.name}
{(instructor.phone) && (
{instructor.phone}
)}
{(instructor.email) && (
{instructor.email}
)}
{instructor.multiArea && (
Willing to Travel
)}
{distDisplay}
mi
{searchCoords && (
Map
)}
{initialData ? 'Edit Instructor' : 'Add New Instructor'}
Dispatcher Pro
Instructor Distance Manager
{searchError && {searchError}}
{searchCoords ? `Instructors near "${searchCoords.query}"` : 'All Instructors'} ({processedInstructors.length})
{processedInstructors.length > 0 ? (
processedInstructors.map(instructor => (
{ setEditingInstructor(instructor); setIsEditing(true); }} onDelete={handleDelete} />
))
) : (
)}
No instructors found.