History Export to CSV
This commit is contained in:
155
src/utils/csvExport.ts
Normal file
155
src/utils/csvExport.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { WorkoutSession, WorkoutSet, ExerciseDef } from '../types';
|
||||
|
||||
/**
|
||||
* Escapes a field for CSV format (wraps in quotes if contains comma, quote or newline)
|
||||
*/
|
||||
const escapeCsvField = (field: any): string => {
|
||||
if (field === null || field === undefined) {
|
||||
return '';
|
||||
}
|
||||
const stringValue = String(field);
|
||||
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n') || stringValue.includes('\r')) {
|
||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return stringValue;
|
||||
};
|
||||
|
||||
export const generateCsv = (sessions: WorkoutSession[], exercises: ExerciseDef[]): string => {
|
||||
// Create a map for quick exercise lookup
|
||||
const exerciseMap = new Map<string, ExerciseDef>();
|
||||
exercises.forEach(ex => exerciseMap.set(ex.id, ex));
|
||||
|
||||
const headers = [
|
||||
// Session Data
|
||||
'Session ID',
|
||||
'Session Start Time',
|
||||
'Session End Time',
|
||||
'Session Duration (Seconds)',
|
||||
'Session Date (YYYY-MM-DD)',
|
||||
'Session Note',
|
||||
'User Body Weight (kg)',
|
||||
'Plan ID',
|
||||
'Plan Name',
|
||||
'Session Type',
|
||||
|
||||
// Set Data
|
||||
'Set ID',
|
||||
'Exercise ID',
|
||||
'Exercise Name',
|
||||
'Exercise Type',
|
||||
'Reps',
|
||||
'Weight (kg)',
|
||||
'Duration (Seconds)',
|
||||
'Distance (Meters)',
|
||||
'Height (cm)',
|
||||
'Body Weight Percentage',
|
||||
'Set Timestamp',
|
||||
'Set Side',
|
||||
'Set Completed',
|
||||
|
||||
// Linked Exercise Data
|
||||
'Linked Exercise Is Unilateral',
|
||||
'Linked Exercise Default Rest (Seconds)',
|
||||
'Linked Exercise Default BW %',
|
||||
'Linked Exercise Archived'
|
||||
];
|
||||
|
||||
const rows: string[] = [headers.join(',')];
|
||||
|
||||
// Sort sessions by date descending (though for raw data logic might not matter, usually newest first is nice, or chronological)
|
||||
// Let's stick to the order passed or sort chronologically?
|
||||
// Usually exports are chronological.
|
||||
const sortedSessions = [...sessions].sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
for (const session of sortedSessions) {
|
||||
const sessionDate = new Date(session.startTime).toISOString().split('T')[0];
|
||||
const sessionDuration = session.endTime ? Math.round((session.endTime - session.startTime) / 1000) : '';
|
||||
|
||||
// If session has no sets, we might still want to export it as a row?
|
||||
// Requirements said "Link one row of the table as 1 set".
|
||||
// If no sets, maybe skip? or output one row with empty set data?
|
||||
// Let's output at least one row if empty, but usually sessions have sets.
|
||||
// If empty sets, we can't really make "one row = 1 set".
|
||||
// But for completeness let's skip empty sessions or put nulls. Users usually have sets.
|
||||
|
||||
if (session.sets.length === 0) {
|
||||
// Optional: Handle empty session
|
||||
const row = [
|
||||
session.id,
|
||||
new Date(session.startTime).toISOString(),
|
||||
session.endTime ? new Date(session.endTime).toISOString() : '',
|
||||
sessionDuration,
|
||||
sessionDate,
|
||||
session.note,
|
||||
session.userBodyWeight,
|
||||
session.planId,
|
||||
session.planName,
|
||||
session.type,
|
||||
// Empty set columns
|
||||
...Array(headers.length - 10).fill('')
|
||||
].map(escapeCsvField).join(',');
|
||||
rows.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const set of session.sets) {
|
||||
const linkedExercise = exerciseMap.get(set.exerciseId);
|
||||
|
||||
const row = [
|
||||
// Session
|
||||
session.id,
|
||||
new Date(session.startTime).toISOString(),
|
||||
session.endTime ? new Date(session.endTime).toISOString() : '',
|
||||
sessionDuration,
|
||||
sessionDate,
|
||||
session.note,
|
||||
session.userBodyWeight,
|
||||
session.planId,
|
||||
session.planName,
|
||||
session.type,
|
||||
|
||||
// Set
|
||||
set.id,
|
||||
set.exerciseId,
|
||||
set.exerciseName,
|
||||
set.type,
|
||||
set.reps,
|
||||
set.weight,
|
||||
set.durationSeconds,
|
||||
set.distanceMeters,
|
||||
set.height,
|
||||
set.bodyWeightPercentage,
|
||||
new Date(set.timestamp).toISOString(),
|
||||
set.side,
|
||||
set.completed,
|
||||
|
||||
// Linked Exercise
|
||||
linkedExercise?.isUnilateral,
|
||||
linkedExercise?.defaultRestSeconds,
|
||||
linkedExercise?.bodyWeightPercentage,
|
||||
linkedExercise?.isArchived
|
||||
].map(escapeCsvField).join(',');
|
||||
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
return rows.join('\n');
|
||||
};
|
||||
|
||||
export const downloadCsv = (content: string, baseFilename: string = 'gymflow_history') => {
|
||||
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create filename with current date
|
||||
const dateStr = new Date().toISOString().split('T')[0];
|
||||
const filename = `${baseFilename}_${dateStr}.csv`;
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
Reference in New Issue
Block a user