144 lines
4.6 KiB
TypeScript
144 lines
4.6 KiB
TypeScript
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 }
|
|
);
|
|
}
|
|
}
|