Edit files
This commit is contained in:
143
app/api/export/csv/route.ts
Normal file
143
app/api/export/csv/route.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user