mirror of
https://github.com/DocNR/POWR.git
synced 2025-05-23 18:32:07 +00:00

Implements database tables and services for workout and template storage: - Updates schema to version 5 with new workout and template tables - Adds WorkoutService for CRUD operations on workouts - Enhances TemplateService for template management - Creates NostrWorkoutService for bidirectional Nostr event handling - Implements React hooks for database access - Connects workout store to database layer for persistence - Improves offline support with publication queue This change ensures workouts and templates are properly saved to SQLite and can be referenced across app sessions, while maintaining Nostr integration for social sharing.
318 lines
9.2 KiB
TypeScript
318 lines
9.2 KiB
TypeScript
// lib/db/services/WorkoutHistoryService.ts
|
|
import { SQLiteDatabase } from 'expo-sqlite';
|
|
import { Workout } from '@/types/workout';
|
|
import { format } from 'date-fns';
|
|
import { DbService } from '../db-service';
|
|
import { WorkoutExercise } from '@/types/exercise'; // Add this import
|
|
|
|
export class WorkoutHistoryService {
|
|
private db: DbService;
|
|
|
|
constructor(database: SQLiteDatabase) {
|
|
this.db = new DbService(database);
|
|
}
|
|
|
|
/**
|
|
* Get all workouts, sorted by date in descending order
|
|
*/
|
|
async getAllWorkouts(): Promise<Workout[]> {
|
|
try {
|
|
const workouts = await this.db.getAllAsync<{
|
|
id: string;
|
|
title: string;
|
|
type: string;
|
|
start_time: number;
|
|
end_time: number | null;
|
|
is_completed: number;
|
|
created_at: number;
|
|
updated_at: number;
|
|
template_id: string | null;
|
|
total_volume: number | null;
|
|
total_reps: number | null;
|
|
source: string;
|
|
notes: string | null;
|
|
}>(
|
|
`SELECT * FROM workouts
|
|
ORDER BY start_time DESC`
|
|
);
|
|
|
|
// Transform database records to Workout objects
|
|
const result: Workout[] = [];
|
|
|
|
for (const workout of workouts) {
|
|
const exercises = await this.getWorkoutExercises(workout.id);
|
|
|
|
result.push({
|
|
id: workout.id,
|
|
title: workout.title,
|
|
type: workout.type as any,
|
|
startTime: workout.start_time,
|
|
endTime: workout.end_time || undefined,
|
|
isCompleted: Boolean(workout.is_completed),
|
|
created_at: workout.created_at,
|
|
lastUpdated: workout.updated_at,
|
|
templateId: workout.template_id || undefined,
|
|
totalVolume: workout.total_volume || undefined,
|
|
totalReps: workout.total_reps || undefined,
|
|
notes: workout.notes || undefined,
|
|
exercises,
|
|
availability: {
|
|
source: [workout.source as any]
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error getting workouts:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get workouts for a specific date
|
|
*/
|
|
async getWorkoutsByDate(date: Date): Promise<Workout[]> {
|
|
try {
|
|
const startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
const endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999).getTime();
|
|
|
|
const workouts = await this.db.getAllAsync<{
|
|
id: string;
|
|
title: string;
|
|
type: string;
|
|
start_time: number;
|
|
end_time: number | null;
|
|
is_completed: number;
|
|
created_at: number;
|
|
updated_at: number;
|
|
template_id: string | null;
|
|
total_volume: number | null;
|
|
total_reps: number | null;
|
|
source: string;
|
|
notes: string | null;
|
|
}>(
|
|
`SELECT * FROM workouts
|
|
WHERE start_time >= ? AND start_time <= ?
|
|
ORDER BY start_time DESC`,
|
|
[startOfDay, endOfDay]
|
|
);
|
|
|
|
const result: Workout[] = [];
|
|
|
|
for (const workout of workouts) {
|
|
const exercises = await this.getWorkoutExercises(workout.id);
|
|
|
|
result.push({
|
|
id: workout.id,
|
|
title: workout.title,
|
|
type: workout.type as any,
|
|
startTime: workout.start_time,
|
|
endTime: workout.end_time || undefined,
|
|
isCompleted: Boolean(workout.is_completed),
|
|
created_at: workout.created_at,
|
|
lastUpdated: workout.updated_at,
|
|
templateId: workout.template_id || undefined,
|
|
totalVolume: workout.total_volume || undefined,
|
|
totalReps: workout.total_reps || undefined,
|
|
notes: workout.notes || undefined,
|
|
exercises,
|
|
availability: {
|
|
source: [workout.source as any]
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error getting workouts by date:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all dates that have workouts within a month
|
|
*/
|
|
async getWorkoutDatesInMonth(year: number, month: number): Promise<Date[]> {
|
|
try {
|
|
const startOfMonth = new Date(year, month, 1).getTime();
|
|
const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59, 999).getTime();
|
|
|
|
const result = await this.db.getAllAsync<{
|
|
start_time: number;
|
|
}>(
|
|
`SELECT DISTINCT date(start_time/1000, 'unixepoch', 'localtime') * 1000 as start_time
|
|
FROM workouts
|
|
WHERE start_time >= ? AND start_time <= ?`,
|
|
[startOfMonth, endOfMonth]
|
|
);
|
|
|
|
return result.map(row => new Date(row.start_time));
|
|
} catch (error) {
|
|
console.error('Error getting workout dates:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get workout details including exercises
|
|
*/
|
|
async getWorkoutDetails(workoutId: string): Promise<Workout | null> {
|
|
try {
|
|
const workout = await this.db.getFirstAsync<{
|
|
id: string;
|
|
title: string;
|
|
type: string;
|
|
start_time: number;
|
|
end_time: number | null;
|
|
is_completed: number;
|
|
created_at: number;
|
|
updated_at: number;
|
|
template_id: string | null;
|
|
total_volume: number | null;
|
|
total_reps: number | null;
|
|
source: string;
|
|
notes: string | null;
|
|
}>(
|
|
`SELECT * FROM workouts WHERE id = ?`,
|
|
[workoutId]
|
|
);
|
|
|
|
if (!workout) return null;
|
|
|
|
// Get exercises for this workout
|
|
const exercises = await this.getWorkoutExercises(workoutId);
|
|
|
|
return {
|
|
id: workout.id,
|
|
title: workout.title,
|
|
type: workout.type as any,
|
|
startTime: workout.start_time,
|
|
endTime: workout.end_time || undefined,
|
|
isCompleted: Boolean(workout.is_completed),
|
|
created_at: workout.created_at,
|
|
lastUpdated: workout.updated_at,
|
|
templateId: workout.template_id || undefined,
|
|
totalVolume: workout.total_volume || undefined,
|
|
totalReps: workout.total_reps || undefined,
|
|
notes: workout.notes || undefined,
|
|
exercises,
|
|
availability: {
|
|
source: [workout.source as any]
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error getting workout details:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the total number of workouts
|
|
*/
|
|
async getWorkoutCount(): Promise<number> {
|
|
try {
|
|
const result = await this.db.getFirstAsync<{ count: number }>(
|
|
`SELECT COUNT(*) as count FROM workouts`
|
|
);
|
|
|
|
return result?.count || 0;
|
|
} catch (error) {
|
|
console.error('Error getting workout count:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Helper method to load workout exercises and sets
|
|
private async getWorkoutExercises(workoutId: string): Promise<WorkoutExercise[]> {
|
|
try {
|
|
const exercises = await this.db.getAllAsync<{
|
|
id: string;
|
|
exercise_id: string;
|
|
display_order: number;
|
|
notes: string | null;
|
|
created_at: number;
|
|
updated_at: number;
|
|
}>(
|
|
`SELECT we.* FROM workout_exercises we
|
|
WHERE we.workout_id = ?
|
|
ORDER BY we.display_order`,
|
|
[workoutId]
|
|
);
|
|
|
|
const result: WorkoutExercise[] = [];
|
|
|
|
for (const exercise of exercises) {
|
|
// Get the base exercise info
|
|
const baseExercise = await this.db.getFirstAsync<{
|
|
title: string;
|
|
type: string;
|
|
category: string;
|
|
equipment: string | null;
|
|
}>(
|
|
`SELECT title, type, category, equipment FROM exercises WHERE id = ?`,
|
|
[exercise.exercise_id]
|
|
);
|
|
|
|
// Get the tags for this exercise
|
|
const tags = await this.db.getAllAsync<{ tag: string }>(
|
|
`SELECT tag FROM exercise_tags WHERE exercise_id = ?`,
|
|
[exercise.exercise_id]
|
|
);
|
|
|
|
// Get the sets for this exercise
|
|
const sets = await this.db.getAllAsync<{
|
|
id: string;
|
|
type: string;
|
|
weight: number | null;
|
|
reps: number | null;
|
|
rpe: number | null;
|
|
duration: number | null;
|
|
is_completed: number;
|
|
completed_at: number | null;
|
|
created_at: number;
|
|
updated_at: number;
|
|
}>(
|
|
`SELECT * FROM workout_sets
|
|
WHERE workout_exercise_id = ?
|
|
ORDER BY id`,
|
|
[exercise.id]
|
|
);
|
|
|
|
// Map sets to the correct format
|
|
const mappedSets = sets.map(set => ({
|
|
id: set.id,
|
|
type: set.type as any,
|
|
weight: set.weight || undefined,
|
|
reps: set.reps || undefined,
|
|
rpe: set.rpe || undefined,
|
|
duration: set.duration || undefined,
|
|
isCompleted: Boolean(set.is_completed),
|
|
completedAt: set.completed_at || undefined,
|
|
lastUpdated: set.updated_at
|
|
}));
|
|
|
|
result.push({
|
|
id: exercise.id,
|
|
exerciseId: exercise.exercise_id,
|
|
title: baseExercise?.title || 'Unknown Exercise',
|
|
type: baseExercise?.type as any || 'strength',
|
|
category: baseExercise?.category as any || 'Other',
|
|
equipment: baseExercise?.equipment as any,
|
|
notes: exercise.notes || undefined,
|
|
tags: tags.map(t => t.tag), // Add the tags array here
|
|
sets: mappedSets,
|
|
created_at: exercise.created_at,
|
|
lastUpdated: exercise.updated_at,
|
|
isCompleted: mappedSets.every(set => set.isCompleted),
|
|
availability: { source: ['local'] }
|
|
});
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('Error getting workout exercises:', error);
|
|
return [];
|
|
}
|
|
}
|
|
} |