const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || ''; interface Stop { eventAddress: string; eventCity: string; eventZip: string; setupTimeStart: Date; setupTimeLatest?: Date; } interface RouteResult { optimizedOrder: number[]; totalDistance: number; totalDuration: number; waypoints: Array<{ address: string; lat: number; lng: number; arrivalTime?: string; setupWindow?: string; }>; } export async function optimizeRouteBySchedule(stops: Stop[]): Promise { if (!GOOGLE_MAPS_API_KEY) { throw new Error('Google Maps API key not configured'); } if (stops.length === 0) { return { optimizedOrder: [], totalDistance: 0, totalDuration: 0, waypoints: [], }; } const stopsWithTimes = stops.map((stop, index) => ({ ...stop, index, setupStart: new Date(stop.setupTimeStart), setupLatest: stop.setupTimeLatest ? new Date(stop.setupTimeLatest) : null, })); stopsWithTimes.sort((a, b) => a.setupStart.getTime() - b.setupStart.getTime()); const sortedIndices = stopsWithTimes.map(s => s.index); let totalDistance = 0; let totalDuration = 0; const waypoints: RouteResult['waypoints'] = []; for (let i = 0; i < stopsWithTimes.length; i++) { const stop = stopsWithTimes[i]; const address = `${stop.eventAddress}, ${stop.eventZip} ${stop.eventCity}`; const geocoded = await geocodeAddress(address); const setupWindow = stop.setupLatest ? `${stop.setupStart.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} - ${stop.setupLatest.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}` : `ab ${stop.setupStart.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`; waypoints.push({ address: `${stop.eventAddress}, ${stop.eventCity}`, lat: geocoded.lat, lng: geocoded.lng, setupWindow, }); if (i > 0) { const prevStop = stopsWithTimes[i - 1]; const origin = `${prevStop.eventAddress}, ${prevStop.eventZip} ${prevStop.eventCity}`; const destination = address; const { distance, duration } = await calculateDistance(origin, destination); totalDistance += distance; totalDuration += duration; } } return { optimizedOrder: sortedIndices, totalDistance, totalDuration, waypoints, }; } export async function optimizeRoute(stops: Stop[]): Promise { if (!GOOGLE_MAPS_API_KEY) { throw new Error('Google Maps API key not configured'); } if (stops.length === 0) { return { optimizedOrder: [], totalDistance: 0, totalDuration: 0, waypoints: [], }; } if (stops.length === 1) { const geocoded = await geocodeAddress( `${stops[0].eventAddress}, ${stops[0].eventZip} ${stops[0].eventCity}` ); return { optimizedOrder: [0], totalDistance: 0, totalDuration: 0, waypoints: [ { address: `${stops[0].eventAddress}, ${stops[0].eventCity}`, lat: geocoded.lat, lng: geocoded.lng, }, ], }; } const origin = `${stops[0].eventAddress}, ${stops[0].eventZip} ${stops[0].eventCity}`; const destination = `${stops[stops.length - 1].eventAddress}, ${stops[stops.length - 1].eventZip} ${stops[stops.length - 1].eventCity}`; const waypoints = stops.slice(1, -1).map(stop => `${stop.eventAddress}, ${stop.eventZip} ${stop.eventCity}` ); const url = new URL('https://maps.googleapis.com/maps/api/directions/json'); url.searchParams.append('origin', origin); url.searchParams.append('destination', destination); if (waypoints.length > 0) { url.searchParams.append('waypoints', `optimize:true|${waypoints.join('|')}`); } url.searchParams.append('key', GOOGLE_MAPS_API_KEY); url.searchParams.append('mode', 'driving'); url.searchParams.append('language', 'de'); const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`Google Maps API error: ${response.statusText}`); } const data = await response.json(); if (data.status !== 'OK') { throw new Error(`Google Maps API returned status: ${data.status}`); } const route = data.routes[0]; const waypointOrder = route.waypoint_order || []; const optimizedOrder = [ 0, ...waypointOrder.map((i: number) => i + 1), stops.length - 1, ]; let totalDistance = 0; let totalDuration = 0; route.legs.forEach((leg: any) => { totalDistance += leg.distance.value; totalDuration += leg.duration.value; }); const waypointsData = await Promise.all( stops.map(async (stop, index) => { const address = `${stop.eventAddress}, ${stop.eventZip} ${stop.eventCity}`; const geocoded = await geocodeAddress(address); return { address: `${stop.eventAddress}, ${stop.eventCity}`, lat: geocoded.lat, lng: geocoded.lng, }; }) ); return { optimizedOrder, totalDistance: Math.round(totalDistance / 1000), totalDuration: Math.round(totalDuration / 60), waypoints: waypointsData, }; } async function geocodeAddress(address: string): Promise<{ lat: number; lng: number }> { const url = new URL('https://maps.googleapis.com/maps/api/geocode/json'); url.searchParams.append('address', address); url.searchParams.append('key', GOOGLE_MAPS_API_KEY); url.searchParams.append('language', 'de'); const response = await fetch(url.toString()); const data = await response.json(); if (data.status === 'OK' && data.results.length > 0) { const location = data.results[0].geometry.location; return { lat: location.lat, lng: location.lng, }; } return { lat: 0, lng: 0 }; } export async function calculateDistance( origin: string, destination: string ): Promise<{ distance: number; duration: number }> { if (!GOOGLE_MAPS_API_KEY) { throw new Error('Google Maps API key not configured'); } const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json'); url.searchParams.append('origins', origin); url.searchParams.append('destinations', destination); url.searchParams.append('key', GOOGLE_MAPS_API_KEY); url.searchParams.append('mode', 'driving'); url.searchParams.append('language', 'de'); const response = await fetch(url.toString()); const data = await response.json(); if (data.status === 'OK' && data.rows[0].elements[0].status === 'OK') { const element = data.rows[0].elements[0]; return { distance: Math.round(element.distance.value / 1000), duration: Math.round(element.duration.value / 60), }; } return { distance: 0, duration: 0 }; }