import { NextRequest, NextResponse } from "next/server"; import { locationDb, userDb } from "@/lib/db"; import { auth } from "@/lib/auth"; import { calculateDistance, RateLimitedGeocoder } from "@/lib/geo-utils"; /** * GET /api/export/csv * * Export location data as CSV for Lexware digital logbook * * Query parameters: * - username: Filter by device tracker ID * - startTime: Custom range start (ISO string) * - endTime: Custom range end (ISO string) * - includeGeocoding: Whether to include addresses (default: false for preview) */ export async function GET(request: NextRequest) { try { // Check authentication const session = await auth(); if (!session?.user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // Get user's allowed device IDs const userId = (session.user as any).id; const role = (session.user as any).role; const sessionUsername = session.user.name || ''; const userDeviceIds = userDb.getAllowedDeviceIds(userId, role, sessionUsername); if (userDeviceIds.length === 0) { return new NextResponse("Keine Geräte verfügbar", { status: 403 }); } const searchParams = request.nextUrl.searchParams; const username = searchParams.get('username') || undefined; const startTime = searchParams.get('startTime') || undefined; const endTime = searchParams.get('endTime') || undefined; const includeGeocoding = searchParams.get('includeGeocoding') === 'true'; // Fetch locations from database let locations = locationDb.findMany({ user_id: 0, username, startTime, endTime, limit: 10000, // High limit for exports }); // Filter by user's allowed devices locations = locations.filter(loc => loc.username && userDeviceIds.includes(loc.username)); if (locations.length === 0) { return new NextResponse("Keine Daten im gewählten Zeitraum", { status: 404 }); } // Sort chronologically (oldest first) for proper distance calculation locations.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); // Initialize geocoder if needed const geocoder = includeGeocoding ? new RateLimitedGeocoder() : null; // Build CSV const csvRows: string[] = []; // Header csvRows.push('Datum,Uhrzeit,Latitude,Longitude,Adresse,Distanz (km),Geschwindigkeit (km/h),Gerät'); // Process each location for (let i = 0; i < locations.length; i++) { const loc = locations[i]; const lat = Number(loc.latitude); const lon = Number(loc.longitude); // Calculate distance from previous point let distance = 0; if (i > 0) { const prevLoc = locations[i - 1]; distance = calculateDistance( Number(prevLoc.latitude), Number(prevLoc.longitude), lat, lon ); } // Geocode if requested let address = `${lat.toFixed(6)}, ${lon.toFixed(6)}`; if (geocoder) { address = await geocoder.geocode(lat, lon); } // Format timestamp (German format) const date = new Date(loc.timestamp); const dateStr = date.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', }); const timeStr = date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit', }); // Speed (may be null) const speed = loc.speed != null ? Number(loc.speed).toFixed(1) : ''; // Device name const deviceName = loc.username || 'Unbekannt'; // Build CSV row - properly escape address field const escapedAddress = address.includes(',') ? `"${address}"` : address; const distanceStr = distance.toFixed(3).replace('.', ','); // German decimal separator csvRows.push( `${dateStr},${timeStr},${lat.toFixed(6)},${lon.toFixed(6)},${escapedAddress},${distanceStr},${speed},${deviceName}` ); } const csv = csvRows.join('\n'); // Return CSV with proper headers const filename = `fahrtenbuch_${startTime ? new Date(startTime).toISOString().split('T')[0] : 'alle'}_${endTime ? new Date(endTime).toISOString().split('T')[0] : 'alle'}.csv`; return new NextResponse(csv, { headers: { 'Content-Type': 'text/csv; charset=utf-8', 'Content-Disposition': `attachment; filename="${filename}"`, }, }); } catch (error) { console.error("Error exporting CSV:", error); return NextResponse.json( { error: "Failed to export CSV", details: error instanceof Error ? error.message : "Unknown error" }, { status: 500 } ); } }