// ===================================================================== // PilotEdge API Client // Fetches data from the backend API and transforms responses to match // the shapes expected by the existing frontend rendering code. // Replace mock-data.js and mock-data-extended.js with this file. // ===================================================================== const API_BASE = '/api'; const PilotEdge = { // Auth token management _token: localStorage.getItem('pilotedge_token'), setToken(token) { this._token = token; if (token) localStorage.setItem('pilotedge_token', token); else localStorage.removeItem('pilotedge_token'); }, getToken() { return this._token; }, getUser() { const raw = localStorage.getItem('pilotedge_user'); return raw ? JSON.parse(raw) : null; }, setUser(user) { if (user) localStorage.setItem('pilotedge_user', JSON.stringify(user)); else localStorage.removeItem('pilotedge_user'); }, // Core fetch wrapper async request(path, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (this._token) headers['Authorization'] = `Bearer ${this._token}`; const res = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (!res.ok) { const body = await res.json().catch(() => ({})); const err = new Error(body.error || `API error ${res.status}`); err.status = res.status; throw err; } return res.json(); }, async get(path) { return this.request(path); }, async post(path, data) { return this.request(path, { method: 'POST', body: JSON.stringify(data) }); }, async put(path, data) { return this.request(path, { method: 'PUT', body: JSON.stringify(data) }); }, async del(path) { return this.request(path, { method: 'DELETE' }); }, // ----------------------------------------------------------------- // Auth // ----------------------------------------------------------------- async register(email, password, name, role) { const res = await this.post('/auth/register', { email, password, name, role }); this.setToken(res.token); this.setUser(res.user); return res; }, async login(email, password) { const res = await this.post('/auth/login', { email, password }); this.setToken(res.token); this.setUser(res.user); return res; }, logout() { this.setToken(null); this.setUser(null); }, async me() { return this.get('/auth/me'); }, // ----------------------------------------------------------------- // Regulations — returns MOCK_STATE_REGULATIONS-compatible shape // ----------------------------------------------------------------- async getRegulations() { const states = await this.get('/regulations'); return states.map(s => ({ name: s.name, abbr: s.abbr, lat: s.lat, lng: s.lng, permitWidth: s.regulation?.permitWidth || '', permitHeight: s.regulation?.permitHeight || '', permitLength: s.regulation?.permitLength || '', permitWeight: s.regulation?.permitWeight || '', escortWidth: s.regulation?.escortWidth || '', escortHeight: s.regulation?.escortHeight || '', escortLength: s.regulation?.escortLength || '', escortWeight: s.regulation?.escortWeight || '', travel: s.regulation?.travelRestrictions || '', holidays: s.regulation?.holidays || '', agency: s.regulation?.agency || '', url: s.regulation?.url || '', notes: s.regulation?.notes || '', })); }, // ----------------------------------------------------------------- // Equipment — returns MOCK_STATE_EQUIPMENT-compatible shape // Object keyed by state abbr: { TX: { escort: {...}, carrier: {...} } } // ----------------------------------------------------------------- async getEquipment() { const states = await this.get('/regulations'); const equipment = {}; for (const s of states) { if (!s.equipmentRequirements) { const full = await this.get(`/regulations/${s.abbr}`); s.equipmentRequirements = full.equipmentRequirements || []; } if (s.equipmentRequirements.length > 0) { equipment[s.abbr] = {}; for (const eq of s.equipmentRequirements) { const obj = { certification: eq.certification || '', vehicle: eq.vehicle || '', signs: eq.signs || '', lights: eq.lights || '', heightPole: eq.heightPole || '', flags: eq.flags || '', communication: eq.communication || '', safety: eq.safetyGear || '', }; if (eq.type === 'escort') equipment[s.abbr].escort = obj; else if (eq.type === 'carrier') { // Parse carrier safetyGear back to individual fields const gear = eq.safetyGear || ''; equipment[s.abbr].carrier = { signs: eq.signs || '', flags: eq.flags || '', lights: eq.lights || '', cones: extractGearField(gear, 'Cones'), fireExtinguisher: extractGearField(gear, 'Fire ext'), triangles: extractGearField(gear, 'Triangles'), flares: extractGearField(gear, 'Flares'), firstAid: extractGearField(gear, 'First aid'), }; } } } } return equipment; }, // ----------------------------------------------------------------- // Contacts — returns MOCK_STATE_CONTACTS-compatible shape // Object keyed by state abbr: { AL: { name, permit, police, email, hours, portal } } // ----------------------------------------------------------------- async getContacts() { const contacts = await this.get('/contacts'); const result = {}; for (const c of contacts) { result[c.state.abbr] = { name: c.state.name, permit: c.permitPhone, police: c.policePhone, email: c.email, hours: c.hours, portal: c.portalUrl, }; } return result; }, // ----------------------------------------------------------------- // Calendar — returns MOCK_SEASONAL_RESTRICTIONS-compatible shape // ----------------------------------------------------------------- async getSeasonalRestrictions() { const restrictions = await this.get('/calendar'); return restrictions.map(r => ({ id: r.id, state: r.state?.abbr || '', stateName: r.state?.name || '', type: r.type, title: r.name, startMonth: r.startMonth, startDay: 1, endMonth: r.endMonth, endDay: 28, description: r.description, color: getRestrictionColor(r.type), routes: '', impact: '', })); }, // ----------------------------------------------------------------- // Truck Stops — returns MOCK_TRUCK_STOPS-compatible shape // ----------------------------------------------------------------- async getTruckStops() { const stops = await this.get('/truckstops'); return stops.map(ts => ({ id: ts.id, name: ts.name, type: 'truck_stop', location: { city: ts.address?.split(',')[0]?.trim() || '', state: ts.state?.abbr || '', lat: ts.lat, lng: ts.lng, }, oversizeFriendly: ts.hasOversizeParking, entranceWidth: ts.entranceWidth || '', entranceHeight: ts.entranceHeight || '', lotSize: ts.lotSqFt ? `${ts.lotSqFt} sq ft` : '', oversizeCapacity: '', facilities: ts.facilities || [], description: '', comments: (ts.contributions || []).map(c => ({ user: c.user?.name || 'Anonymous', date: c.createdAt, text: c.content, })), })); }, // ----------------------------------------------------------------- // Bridges — returns MOCK_BRIDGE_CLEARANCES-compatible shape // ----------------------------------------------------------------- async getBridges() { const bridges = await this.get('/bridges'); return bridges.map(b => ({ id: b.id, route: b.route, mileMarker: '', type: b.name.split(' at ')[0] || 'Bridge', location: { desc: b.name.split(' at ')[1] || b.name, city: '', state: b.state?.abbr || '', lat: b.lat, lng: b.lng, }, clearanceHeight: `${b.heightClearance}'`, clearanceWidth: b.widthClearance ? `${b.widthClearance}'` : 'Unrestricted', weightLimit: b.weightLimit ? `${b.weightLimit} lbs` : 'No posted limit', notes: '', })); }, // ----------------------------------------------------------------- // Weigh Stations — returns MOCK_WEIGH_STATIONS-compatible shape // ----------------------------------------------------------------- async getWeighStations() { const stations = await this.get('/weighstations'); return stations.map(ws => ({ id: ws.id, name: ws.name, route: ws.route, location: { city: '', state: ws.state?.abbr || '', lat: ws.lat, lng: ws.lng, }, hours: ws.hours, prePass: ws.prePass, currentStatus: ws.currentStatus, direction: ws.direction, lastFlagged: ws.lastStatusUpdate || null, flaggedBy: '', notes: '', })); }, // ----------------------------------------------------------------- // Alerts — returns MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS shapes // ----------------------------------------------------------------- async getAlerts() { const alerts = await this.get('/alerts'); const routeConditions = []; const weatherAlerts = []; for (const a of alerts) { if (a.type === 'weather' || a.type === 'wind') { weatherAlerts.push({ id: a.id, type: a.type, severity: a.severity, region: a.state?.name || '', routes: a.route ? a.route.split(', ') : [], description: a.description, validFrom: a.startsAt, validTo: a.endsAt, source: 'NWS', lat: 0, lng: 0, }); } else { routeConditions.push({ id: a.id, type: a.type, severity: a.severity, route: a.route, location: { desc: a.description.substring(0, 60), state: a.state?.abbr || '', lat: 0, lng: 0, }, description: a.description, startDate: a.startsAt, endDate: a.endsAt, source: 'State DOT', affectsOversize: true, }); } } return { routeConditions, weatherAlerts }; }, // ----------------------------------------------------------------- // Load Board — returns MOCK_LOAD_BOARD-compatible shape // ----------------------------------------------------------------- async getLoads() { const data = await this.get('/loads?limit=100'); return (data.loads || []).map(l => ({ id: l.id, carrier: l.poster?.name || 'Unknown', origin: parseLocation(l.origin), destination: parseLocation(l.destination), departureDate: l.pickupDate, dimensions: { width: l.width, height: l.height, length: l.length, weight: l.weight, }, description: l.description, escortsNeeded: l.escortsNeeded, status: l.status, postedDate: l.createdAt, contact: l.poster?.name || '', })); }, // ----------------------------------------------------------------- // Escort Operators — returns MOCK_ESCORT_OPERATORS-compatible shape // ----------------------------------------------------------------- async getEscortOperators() { const escorts = await this.get('/escorts'); return escorts.map(e => ({ id: e.id, name: e.user?.name || 'Unknown', location: { city: '', state: '', lat: e.lat, lng: e.lng, }, status: e.availability, certifications: e.certifications || [], vehicleType: e.vehicleType, rating: e.rating, totalJobs: e.ratingCount || 0, experience: '', contact: e.user?.email || '', phone: e.phone, bio: e.bio, })); }, // ----------------------------------------------------------------- // Documents — returns MOCK_DOCUMENTS-compatible shape // ----------------------------------------------------------------- async getDocuments() { try { const docs = await this.get('/documents'); return docs.map(d => ({ id: d.id, name: d.filename, type: d.type, state: '', uploadDate: d.createdAt, expiryDate: d.expiresAt, fileSize: formatFileSize(d.sizeBytes), status: d.expiresAt && new Date(d.expiresAt) < new Date() ? 'expired' : 'active', })); } catch (err) { // If not authenticated, return empty array if (err.status === 401) return []; throw err; } }, // ----------------------------------------------------------------- // Orders // ----------------------------------------------------------------- async submitOrder(orderData) { return this.post('/orders', orderData); }, async getOrders() { return this.get('/orders'); }, // ----------------------------------------------------------------- // Contributions // ----------------------------------------------------------------- async submitContribution(entityType, entityId, type, content) { return this.post('/contributions', { entityType, entityId, type, content }); }, }; // ----------------------------------------------------------------- // Helper functions // ----------------------------------------------------------------- function extractGearField(gear, label) { const match = gear.match(new RegExp(`${label}:\\s*([^;]+)`)); return match ? match[1].trim() : ''; } function getRestrictionColor(type) { const colors = { spring_weight: '#3b82f6', winter_closure: '#8b5cf6', harvest: '#f59e0b', holiday_blackout: '#ef4444', }; return colors[type] || '#6b7280'; } function parseLocation(str) { // Parse "City, ST" into { city, state, lat: 0, lng: 0 } if (!str) return { city: '', state: '', lat: 0, lng: 0 }; const parts = str.split(',').map(s => s.trim()); return { city: parts[0] || '', state: parts[1] || '', lat: 0, lng: 0, }; } function formatFileSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let size = bytes; while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; } return `${Math.round(size * 10) / 10} ${units[i]}`; }