Files
proof-of-work/proof-of-work/app/api/settings/export-csv/route.ts
T
Keysat 4be489d6d3 v1.2.0:5 — Gear (breathing, 1-5) replaces RPE as the effort field for cardio
Cardio exercises now log a breathing "Gear" (1-5, per Brian MacKenzie)
instead of RPE (6-10) as their effort field; strength keeps RPE. An exercise
counts as cardio when its equipment type is "cardio" or it carries the
"cardio" muscle group (isCardioExercise in lib/exerciseOptions), so the
Assault Bike (type "assault bike") qualifies.

New nullable SetLog.gear column added by the boot-time guarded ALTER in
docker_entrypoint.sh (additive, idempotent); plumbed through all 5 set-write
paths, the summary/edit views, and CSV/JSON import-export. Existing rpe data
is untouched and still displays. Program/AI target-RPE is unaffected.
2026-06-16 14:49:15 -05:00

135 lines
3.2 KiB
TypeScript

import { getCurrentUser } from "@/lib/auth";
import { getTimestampFileSuffix } from "@/lib/db-file";
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
function csvCell(value: unknown): string {
if (value === null || value === undefined) return "";
const text = String(value);
if (text.includes(",") || text.includes("\"") || text.includes("\n")) {
return `"${text.replace(/"/g, "\"\"")}"`;
}
return text;
}
/**
* GET /api/settings/export-csv
* Exports workout + set-level rows to CSV for offline backup.
*/
export async function GET() {
try {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const setLogs = await prisma.setLog.findMany({
where: {
workout: {
userId: user.id,
deletedAt: null,
},
},
include: {
exercise: true,
workout: {
select: {
id: true,
date: true,
name: true,
notes: true,
durationMinutes: true,
difficulty: true,
caloriesBurned: true,
},
},
},
orderBy: [
{ workout: { date: "desc" } },
{ setNumber: "asc" },
],
});
const header = [
"workoutId",
"workoutDate",
"workoutName",
"workoutNotes",
"workoutDurationMinutes",
"workoutDifficulty",
"workoutCaloriesBurned",
"exerciseId",
"exerciseName",
"setNumber",
"reps",
"weight",
"weightUnit",
"durationMinutes",
"distance",
"distanceUnit",
"setCalories",
"setWatts",
"rpe",
"setGear",
"setNotes",
"customMetricsJson",
];
const lines = [header.join(",")];
for (const set of setLogs) {
const durationMinutes =
typeof set.durationSeconds === "number"
? (set.durationSeconds / 60).toFixed(2)
: "";
const row = [
set.workout.id,
set.workout.date.toISOString(),
set.workout.name ?? "",
set.workout.notes ?? "",
set.workout.durationMinutes ?? "",
set.workout.difficulty ?? "",
set.workout.caloriesBurned ?? "",
set.exerciseId,
set.exercise.name,
set.setNumber,
set.reps ?? "",
set.weight ?? "",
set.weightUnit ?? "",
durationMinutes,
set.distance ?? "",
set.distanceUnit ?? "",
set.calories ?? "",
set.watts ?? "",
set.rpe ?? "",
set.gear ?? "",
set.notes ?? "",
set.customMetrics ?? "",
];
lines.push(row.map(csvCell).join(","));
}
const csv = lines.join("\n");
const fileName = `proof-of-work-export-${getTimestampFileSuffix()}.csv`;
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="${fileName}"`,
"Cache-Control": "no-store",
},
});
} catch (error) {
console.error("CSV export error:", error);
return NextResponse.json(
{ error: "Failed to export CSV" },
{ status: 500 }
);
}
}