mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
Fix history tab navigation issues and improve workout detail error handling
This commit is contained in:
parent
e4aa59a07e
commit
3004bcd4f8
24
CHANGELOG.md
24
CHANGELOG.md
@ -5,6 +5,30 @@ All notable changes to the POWR project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
# Changelog - March 23, 2025
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
- History tab navigation issues
|
||||||
|
- Fixed nested screens warning by renaming "history" screen to "workouts" in history tab
|
||||||
|
- Updated initialRouteName to match the new screen name
|
||||||
|
- Improved navigation between history tab and workout detail screen
|
||||||
|
- Workout detail screen improvements
|
||||||
|
- Added timeout to prevent infinite loading state
|
||||||
|
- Enhanced error handling with proper error state display
|
||||||
|
- Added "Go Back" button for error recovery
|
||||||
|
- Fixed TypeScript errors with proper imports
|
||||||
|
- Enhanced workout history service
|
||||||
|
- Added detailed logging for exercise loading process
|
||||||
|
- Added checks to verify if exercises exist in the database
|
||||||
|
- Fixed TypeScript errors in exercise existence checks
|
||||||
|
- Improved error handling throughout the service
|
||||||
|
|
||||||
|
## Improved
|
||||||
|
- Enhanced debugging capabilities
|
||||||
|
- Added comprehensive logging in EnhancedWorkoutHistoryService
|
||||||
|
- Improved error state handling in workout detail screen
|
||||||
|
- Better error messages for troubleshooting
|
||||||
|
|
||||||
# Changelog - March 22, 2025
|
# Changelog - March 22, 2025
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
@ -17,7 +17,7 @@ export default function HistoryLayout() {
|
|||||||
<Header useLogo={true} showNotifications={true} />
|
<Header useLogo={true} showNotifications={true} />
|
||||||
|
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName="history"
|
initialRouteName="workouts"
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
tabBarActiveTintColor: theme.colors.tabIndicator,
|
tabBarActiveTintColor: theme.colors.tabIndicator,
|
||||||
tabBarInactiveTintColor: theme.colors.tabInactive,
|
tabBarInactiveTintColor: theme.colors.tabInactive,
|
||||||
@ -41,7 +41,7 @@ export default function HistoryLayout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="history"
|
name="workouts" // Changed from "history"
|
||||||
component={HistoryScreen}
|
component={HistoryScreen}
|
||||||
options={{ title: 'History' }}
|
options={{ title: 'History' }}
|
||||||
/>
|
/>
|
||||||
|
240
app/(workout)/workout/[id].tsx
Normal file
240
app/(workout)/workout/[id].tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
// app/(workout)/workout/[id].tsx
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { View, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||||
|
import { Text } from '@/components/ui/text';
|
||||||
|
import { useLocalSearchParams, Stack, useRouter } from 'expo-router';
|
||||||
|
import { useSQLiteContext } from 'expo-sqlite';
|
||||||
|
import { WorkoutHistoryService } from '@/lib/db/services/EnhancedWorkoutHistoryService';
|
||||||
|
import WorkoutDetailView from '@/components/workout/WorkoutDetailView';
|
||||||
|
import { Workout } from '@/types/workout';
|
||||||
|
import { useNDK, useNDKAuth, useNDKEvents } from '@/lib/hooks/useNDK';
|
||||||
|
import { NostrWorkoutService } from '@/lib/db/services/NostrWorkoutService';
|
||||||
|
import { useNDKStore } from '@/lib/stores/ndk';
|
||||||
|
import { Share } from 'react-native';
|
||||||
|
|
||||||
|
export default function WorkoutDetailScreen() {
|
||||||
|
// Add error state
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
const router = useRouter();
|
||||||
|
const db = useSQLiteContext();
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { isAuthenticated } = useNDKAuth();
|
||||||
|
const { publishEvent } = useNDKEvents();
|
||||||
|
|
||||||
|
const [workout, setWorkout] = useState<Workout | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isPublishing, setIsPublishing] = useState(false);
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [isExporting, setIsExporting] = useState(false);
|
||||||
|
|
||||||
|
// Initialize service
|
||||||
|
const workoutHistoryService = React.useMemo(() => new WorkoutHistoryService(db), [db]);
|
||||||
|
|
||||||
|
// Load workout details
|
||||||
|
useEffect(() => {
|
||||||
|
const loadWorkout = async () => {
|
||||||
|
if (!id) {
|
||||||
|
console.log('No workout ID provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Loading workout details for ID: ${id}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null); // Reset error state
|
||||||
|
console.log('Calling workoutHistoryService.getWorkoutDetails...');
|
||||||
|
|
||||||
|
// Add timeout to prevent infinite loading
|
||||||
|
const timeoutPromise = new Promise<null>((_, reject) => {
|
||||||
|
setTimeout(() => reject(new Error('Timeout loading workout details')), 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Race the workout details fetch against the timeout
|
||||||
|
const workoutDetails = await Promise.race([
|
||||||
|
workoutHistoryService.getWorkoutDetails(id),
|
||||||
|
timeoutPromise
|
||||||
|
]) as Workout | null;
|
||||||
|
|
||||||
|
console.log('getWorkoutDetails returned:', workoutDetails ? 'workout found' : 'workout not found');
|
||||||
|
|
||||||
|
if (workoutDetails) {
|
||||||
|
console.log(`Workout title: ${workoutDetails.title}`);
|
||||||
|
console.log(`Workout has ${workoutDetails.exercises?.length || 0} exercises`);
|
||||||
|
setWorkout(workoutDetails);
|
||||||
|
} else {
|
||||||
|
console.error('Workout not found');
|
||||||
|
setError('Workout not found. It may have been deleted.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading workout:', error);
|
||||||
|
setError(`Error loading workout: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
} finally {
|
||||||
|
console.log('Setting isLoading to false');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadWorkout();
|
||||||
|
}, [id, workoutHistoryService]);
|
||||||
|
|
||||||
|
// Handle publishing to Nostr
|
||||||
|
const handlePublish = async () => {
|
||||||
|
if (!workout || !ndk || !isAuthenticated) {
|
||||||
|
alert('You need to be logged in to Nostr to publish workouts');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsPublishing(true);
|
||||||
|
|
||||||
|
// Create Nostr event
|
||||||
|
const nostrEvent = NostrWorkoutService.createCompleteWorkoutEvent(workout);
|
||||||
|
|
||||||
|
// Publish event using the kind, content, and tags from the created event
|
||||||
|
const publishedEvent = await publishEvent(
|
||||||
|
nostrEvent.kind,
|
||||||
|
nostrEvent.content,
|
||||||
|
nostrEvent.tags
|
||||||
|
);
|
||||||
|
|
||||||
|
if (publishedEvent?.id) {
|
||||||
|
// Update local database with Nostr event ID
|
||||||
|
const relayCount = ndk.pool?.relays.size || 0;
|
||||||
|
|
||||||
|
// Update workout in memory
|
||||||
|
setWorkout({
|
||||||
|
...workout,
|
||||||
|
availability: {
|
||||||
|
...workout.availability,
|
||||||
|
nostrEventId: publishedEvent.id,
|
||||||
|
nostrPublishedAt: Date.now(),
|
||||||
|
nostrRelayCount: relayCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Workout published to Nostr with event ID: ${publishedEvent.id}`);
|
||||||
|
alert('Workout published successfully!');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error publishing workout to Nostr:', error);
|
||||||
|
alert('Failed to publish workout. Please try again.');
|
||||||
|
} finally {
|
||||||
|
setIsPublishing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle importing from Nostr to local
|
||||||
|
const handleImport = async () => {
|
||||||
|
if (!workout) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsImporting(true);
|
||||||
|
|
||||||
|
// Import workout to local database
|
||||||
|
// This would be implemented in a future version
|
||||||
|
console.log('Importing workout from Nostr to local database');
|
||||||
|
|
||||||
|
// For now, just show a message
|
||||||
|
alert('Workout import functionality will be available in a future update');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing workout:', error);
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle exporting workout data
|
||||||
|
const handleExport = async (format: 'csv' | 'json') => {
|
||||||
|
if (!workout) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsExporting(true);
|
||||||
|
|
||||||
|
// Create export data
|
||||||
|
let exportData = '';
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
// Export as JSON
|
||||||
|
exportData = JSON.stringify(workout, null, 2);
|
||||||
|
} else {
|
||||||
|
// Export as CSV
|
||||||
|
const headers = 'ID,Title,Type,Start Time,End Time,Completed,Volume,Reps,Notes\n';
|
||||||
|
const row = [
|
||||||
|
workout.id,
|
||||||
|
`"${workout.title.replace(/"/g, '""')}"`, // Escape quotes in title
|
||||||
|
workout.type,
|
||||||
|
new Date(workout.startTime).toISOString(),
|
||||||
|
workout.endTime ? new Date(workout.endTime).toISOString() : '',
|
||||||
|
workout.isCompleted ? 'Yes' : 'No',
|
||||||
|
workout.totalVolume || '',
|
||||||
|
workout.totalReps || '',
|
||||||
|
workout.notes ? `"${workout.notes.replace(/"/g, '""')}"` : '' // Escape quotes in notes
|
||||||
|
];
|
||||||
|
|
||||||
|
exportData = headers + row.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Share the exported data
|
||||||
|
await Share.share({
|
||||||
|
message: exportData,
|
||||||
|
title: `${workout.title} - Workout Export (${format.toUpperCase()})`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error exporting workout as ${format}:`, error);
|
||||||
|
alert(`Failed to export workout as ${format}. Please try again.`);
|
||||||
|
} finally {
|
||||||
|
setIsExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className="flex-1 bg-background">
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: workout?.title || 'Workout Details',
|
||||||
|
headerShown: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<View className="flex-1 items-center justify-center">
|
||||||
|
<ActivityIndicator size="large" className="mb-4" />
|
||||||
|
<Text className="text-muted-foreground">Loading workout details...</Text>
|
||||||
|
</View>
|
||||||
|
) : error ? (
|
||||||
|
<View className="flex-1 items-center justify-center p-4">
|
||||||
|
<Text className="text-foreground text-lg mb-2">Error</Text>
|
||||||
|
<Text className="text-muted-foreground text-center mb-4">{error}</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
className="bg-primary px-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
<Text className="text-primary-foreground">Go Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : workout ? (
|
||||||
|
<WorkoutDetailView
|
||||||
|
workout={workout}
|
||||||
|
onPublish={handlePublish}
|
||||||
|
onImport={handleImport}
|
||||||
|
onExport={handleExport}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View className="flex-1 items-center justify-center p-4">
|
||||||
|
<Text className="text-foreground text-lg mb-2">Workout not found</Text>
|
||||||
|
<Text className="text-muted-foreground text-center mb-4">
|
||||||
|
The workout you're looking for could not be found or may have been deleted.
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
className="bg-primary px-4 py-2 rounded-md"
|
||||||
|
>
|
||||||
|
<Text className="text-primary-foreground">Go Back</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
758
lib/db/services/EnhancedWorkoutHistoryService.ts
Normal file
758
lib/db/services/EnhancedWorkoutHistoryService.ts
Normal file
@ -0,0 +1,758 @@
|
|||||||
|
// lib/db/services/EnhancedWorkoutHistoryService.ts
|
||||||
|
import { SQLiteDatabase } from 'expo-sqlite';
|
||||||
|
import { Workout } from '@/types/workout';
|
||||||
|
import { format, startOfDay, endOfDay, startOfMonth, endOfMonth } from 'date-fns';
|
||||||
|
import { DbService } from '../db-service';
|
||||||
|
import { WorkoutExercise } from '@/types/exercise';
|
||||||
|
|
||||||
|
// Define workout filter interface
|
||||||
|
export interface WorkoutFilters {
|
||||||
|
type?: string[];
|
||||||
|
dateRange?: { start: Date; end: Date };
|
||||||
|
exercises?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
source?: ('local' | 'nostr' | 'both')[];
|
||||||
|
searchTerm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define workout sync status interface
|
||||||
|
export interface WorkoutSyncStatus {
|
||||||
|
isLocal: boolean;
|
||||||
|
isPublished: boolean;
|
||||||
|
eventId?: string;
|
||||||
|
relayCount?: number;
|
||||||
|
lastPublished?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define export format type
|
||||||
|
export type ExportFormat = 'csv' | 'json';
|
||||||
|
|
||||||
|
export class WorkoutHistoryService {
|
||||||
|
private db: DbService;
|
||||||
|
|
||||||
|
constructor(database: SQLiteDatabase) {
|
||||||
|
this.db = new DbService(database);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search workouts by title, exercise name, or notes
|
||||||
|
*/
|
||||||
|
async searchWorkouts(query: string): Promise<Workout[]> {
|
||||||
|
try {
|
||||||
|
if (!query || query.trim() === '') {
|
||||||
|
return this.getAllWorkouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = `%${query.trim()}%`;
|
||||||
|
|
||||||
|
// Search in workout titles and notes
|
||||||
|
const workoutIds = await this.db.getAllAsync<{ id: string }>(
|
||||||
|
`SELECT DISTINCT w.id
|
||||||
|
FROM workouts w
|
||||||
|
LEFT JOIN workout_exercises we ON w.id = we.workout_id
|
||||||
|
LEFT JOIN exercises e ON we.exercise_id = e.id
|
||||||
|
WHERE w.title LIKE ?
|
||||||
|
OR w.notes LIKE ?
|
||||||
|
OR e.title LIKE ?
|
||||||
|
ORDER BY w.start_time DESC`,
|
||||||
|
[searchTerm, searchTerm, searchTerm]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get full workout details for each matching ID
|
||||||
|
const result: Workout[] = [];
|
||||||
|
for (const { id } of workoutIds) {
|
||||||
|
const workout = await this.getWorkoutDetails(id);
|
||||||
|
if (workout) {
|
||||||
|
result.push(workout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching workouts:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter workouts based on various criteria
|
||||||
|
*/
|
||||||
|
async filterWorkouts(filters: WorkoutFilters): Promise<Workout[]> {
|
||||||
|
try {
|
||||||
|
// Start with a base query
|
||||||
|
let query = `
|
||||||
|
SELECT DISTINCT w.*
|
||||||
|
FROM workouts w
|
||||||
|
`;
|
||||||
|
|
||||||
|
const params: any[] = [];
|
||||||
|
const conditions: string[] = [];
|
||||||
|
|
||||||
|
// Add joins if needed
|
||||||
|
if (filters.exercises && filters.exercises.length > 0) {
|
||||||
|
query += `
|
||||||
|
JOIN workout_exercises we ON w.id = we.workout_id
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.tags && filters.tags.length > 0) {
|
||||||
|
query += `
|
||||||
|
JOIN workout_exercises we2 ON w.id = we2.workout_id
|
||||||
|
JOIN exercise_tags et ON we2.exercise_id = et.exercise_id
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add type filter
|
||||||
|
if (filters.type && filters.type.length > 0) {
|
||||||
|
conditions.push(`w.type IN (${filters.type.map(() => '?').join(', ')})`);
|
||||||
|
params.push(...filters.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add date range filter
|
||||||
|
if (filters.dateRange) {
|
||||||
|
const { start, end } = filters.dateRange;
|
||||||
|
conditions.push('w.start_time >= ? AND w.start_time <= ?');
|
||||||
|
params.push(startOfDay(start).getTime(), endOfDay(end).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add exercise filter
|
||||||
|
if (filters.exercises && filters.exercises.length > 0) {
|
||||||
|
conditions.push(`we.exercise_id IN (${filters.exercises.map(() => '?').join(', ')})`);
|
||||||
|
params.push(...filters.exercises);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tag filter
|
||||||
|
if (filters.tags && filters.tags.length > 0) {
|
||||||
|
conditions.push(`et.tag IN (${filters.tags.map(() => '?').join(', ')})`);
|
||||||
|
params.push(...filters.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add source filter
|
||||||
|
if (filters.source && filters.source.length > 0) {
|
||||||
|
// Handle 'both' specially
|
||||||
|
if (filters.source.includes('both')) {
|
||||||
|
conditions.push(`(w.source = 'local' OR w.source = 'nostr')`);
|
||||||
|
} else {
|
||||||
|
conditions.push(`w.source IN (${filters.source.map(() => '?').join(', ')})`);
|
||||||
|
params.push(...filters.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search term filter
|
||||||
|
if (filters.searchTerm && filters.searchTerm.trim() !== '') {
|
||||||
|
const searchTerm = `%${filters.searchTerm.trim()}%`;
|
||||||
|
conditions.push('(w.title LIKE ? OR w.notes LIKE ?)');
|
||||||
|
params.push(searchTerm, searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add WHERE clause if there are conditions
|
||||||
|
if (conditions.length > 0) {
|
||||||
|
query += ` WHERE ${conditions.join(' AND ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ORDER BY
|
||||||
|
query += ' ORDER BY w.start_time DESC';
|
||||||
|
|
||||||
|
// Execute the query
|
||||||
|
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;
|
||||||
|
nostr_event_id: string | null;
|
||||||
|
nostr_published_at: number | null;
|
||||||
|
nostr_relay_count: number | null;
|
||||||
|
}>(query, params);
|
||||||
|
|
||||||
|
// 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],
|
||||||
|
nostrEventId: workout.nostr_event_id || undefined,
|
||||||
|
nostrPublishedAt: workout.nostr_published_at || undefined,
|
||||||
|
nostrRelayCount: workout.nostr_relay_count || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error filtering workouts:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workouts that include a specific exercise
|
||||||
|
*/
|
||||||
|
async getWorkoutsByExercise(exerciseId: string): Promise<Workout[]> {
|
||||||
|
try {
|
||||||
|
const workoutIds = await this.db.getAllAsync<{ workout_id: string }>(
|
||||||
|
`SELECT DISTINCT workout_id
|
||||||
|
FROM workout_exercises
|
||||||
|
WHERE exercise_id = ?
|
||||||
|
ORDER BY created_at DESC`,
|
||||||
|
[exerciseId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const result: Workout[] = [];
|
||||||
|
for (const { workout_id } of workoutIds) {
|
||||||
|
const workout = await this.getWorkoutDetails(workout_id);
|
||||||
|
if (workout) {
|
||||||
|
result.push(workout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting workouts by exercise:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export workout history in the specified format
|
||||||
|
*/
|
||||||
|
async exportWorkoutHistory(format: ExportFormat): Promise<string> {
|
||||||
|
try {
|
||||||
|
const workouts = await this.getAllWorkouts();
|
||||||
|
|
||||||
|
if (format === 'json') {
|
||||||
|
return JSON.stringify(workouts, null, 2);
|
||||||
|
} else if (format === 'csv') {
|
||||||
|
// Create CSV header
|
||||||
|
let csv = 'ID,Title,Type,Start Time,End Time,Completed,Volume,Reps,Notes\n';
|
||||||
|
|
||||||
|
// Add workout rows
|
||||||
|
for (const workout of workouts) {
|
||||||
|
const row = [
|
||||||
|
workout.id,
|
||||||
|
`"${workout.title.replace(/"/g, '""')}"`, // Escape quotes in title
|
||||||
|
workout.type,
|
||||||
|
new Date(workout.startTime).toISOString(),
|
||||||
|
workout.endTime ? new Date(workout.endTime).toISOString() : '',
|
||||||
|
workout.isCompleted ? 'Yes' : 'No',
|
||||||
|
workout.totalVolume || '',
|
||||||
|
workout.totalReps || '',
|
||||||
|
workout.notes ? `"${workout.notes.replace(/"/g, '""')}"` : '' // Escape quotes in notes
|
||||||
|
];
|
||||||
|
|
||||||
|
csv += row.join(',') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return csv;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported export format: ${format}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting workout history:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workout streak data
|
||||||
|
*/
|
||||||
|
async getWorkoutStreak(): Promise<{ current: number; longest: number; lastWorkoutDate: Date | null }> {
|
||||||
|
try {
|
||||||
|
// Get all workout dates sorted by date
|
||||||
|
const workoutDates = await this.db.getAllAsync<{ workout_date: number }>(
|
||||||
|
`SELECT DISTINCT date(start_time/1000, 'unixepoch', 'localtime') * 1000 as workout_date
|
||||||
|
FROM workouts
|
||||||
|
ORDER BY workout_date DESC`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (workoutDates.length === 0) {
|
||||||
|
return { current: 0, longest: 0, lastWorkoutDate: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = workoutDates.map(row => new Date(row.workout_date));
|
||||||
|
|
||||||
|
// Calculate current streak
|
||||||
|
let currentStreak = 1;
|
||||||
|
let lastDate = dates[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < dates.length; i++) {
|
||||||
|
const currentDate = dates[i];
|
||||||
|
const dayDiff = Math.floor((lastDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (dayDiff === 1) {
|
||||||
|
// Consecutive day
|
||||||
|
currentStreak++;
|
||||||
|
lastDate = currentDate;
|
||||||
|
} else if (dayDiff > 1) {
|
||||||
|
// Streak broken
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate longest streak
|
||||||
|
let longestStreak = 1;
|
||||||
|
let currentLongestStreak = 1;
|
||||||
|
lastDate = dates[0];
|
||||||
|
|
||||||
|
for (let i = 1; i < dates.length; i++) {
|
||||||
|
const currentDate = dates[i];
|
||||||
|
const dayDiff = Math.floor((lastDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (dayDiff === 1) {
|
||||||
|
// Consecutive day
|
||||||
|
currentLongestStreak++;
|
||||||
|
lastDate = currentDate;
|
||||||
|
|
||||||
|
if (currentLongestStreak > longestStreak) {
|
||||||
|
longestStreak = currentLongestStreak;
|
||||||
|
}
|
||||||
|
} else if (dayDiff > 1) {
|
||||||
|
// Streak broken
|
||||||
|
currentLongestStreak = 1;
|
||||||
|
lastDate = currentDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: currentStreak,
|
||||||
|
longest: longestStreak,
|
||||||
|
lastWorkoutDate: dates[0]
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting workout streak:', error);
|
||||||
|
return { current: 0, longest: 0, lastWorkoutDate: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workout sync status
|
||||||
|
*/
|
||||||
|
async getWorkoutSyncStatus(workoutId: string): Promise<WorkoutSyncStatus> {
|
||||||
|
try {
|
||||||
|
const workout = await this.db.getFirstAsync<{
|
||||||
|
source: string;
|
||||||
|
nostr_event_id: string | null;
|
||||||
|
nostr_published_at: number | null;
|
||||||
|
nostr_relay_count: number | null;
|
||||||
|
}>(
|
||||||
|
`SELECT source, nostr_event_id, nostr_published_at, nostr_relay_count
|
||||||
|
FROM workouts
|
||||||
|
WHERE id = ?`,
|
||||||
|
[workoutId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workout) {
|
||||||
|
return { isLocal: false, isPublished: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLocal: workout.source === 'local' || workout.source === 'both',
|
||||||
|
isPublished: Boolean(workout.nostr_event_id),
|
||||||
|
eventId: workout.nostr_event_id || undefined,
|
||||||
|
relayCount: workout.nostr_relay_count || undefined,
|
||||||
|
lastPublished: workout.nostr_published_at || undefined
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting workout sync status:', error);
|
||||||
|
return { isLocal: false, isPublished: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a workout to Nostr
|
||||||
|
* This method updates the database with Nostr publication details
|
||||||
|
* The actual publishing is handled by NostrWorkoutService
|
||||||
|
*/
|
||||||
|
async updateWorkoutNostrStatus(
|
||||||
|
workoutId: string,
|
||||||
|
eventId: string,
|
||||||
|
relayCount: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.db.runAsync(
|
||||||
|
`UPDATE workouts
|
||||||
|
SET nostr_event_id = ?,
|
||||||
|
nostr_published_at = ?,
|
||||||
|
nostr_relay_count = ?
|
||||||
|
WHERE id = ?`,
|
||||||
|
[eventId, Date.now(), relayCount, workoutId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating workout Nostr status:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
nostr_event_id: string | null;
|
||||||
|
nostr_published_at: number | null;
|
||||||
|
nostr_relay_count: number | 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],
|
||||||
|
nostrEventId: workout.nostr_event_id || undefined,
|
||||||
|
nostrPublishedAt: workout.nostr_published_at || undefined,
|
||||||
|
nostrRelayCount: workout.nostr_relay_count || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 startOfDayTime = startOfDay(date).getTime();
|
||||||
|
const endOfDayTime = endOfDay(date).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;
|
||||||
|
nostr_event_id: string | null;
|
||||||
|
nostr_published_at: number | null;
|
||||||
|
nostr_relay_count: number | null;
|
||||||
|
}>(
|
||||||
|
`SELECT * FROM workouts
|
||||||
|
WHERE start_time >= ? AND start_time <= ?
|
||||||
|
ORDER BY start_time DESC`,
|
||||||
|
[startOfDayTime, endOfDayTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
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],
|
||||||
|
nostrEventId: workout.nostr_event_id || undefined,
|
||||||
|
nostrPublishedAt: workout.nostr_published_at || undefined,
|
||||||
|
nostrRelayCount: workout.nostr_relay_count || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 startOfMonthTime = startOfMonth(new Date(year, month)).getTime();
|
||||||
|
const endOfMonthTime = endOfMonth(new Date(year, month)).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 <= ?`,
|
||||||
|
[startOfMonthTime, endOfMonthTime]
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
nostr_event_id: string | null;
|
||||||
|
nostr_published_at: number | null;
|
||||||
|
nostr_relay_count: number | 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],
|
||||||
|
nostrEventId: workout.nostr_event_id || undefined,
|
||||||
|
nostrPublishedAt: workout.nostr_published_at || undefined,
|
||||||
|
nostrRelayCount: workout.nostr_relay_count || undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} 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 {
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Getting exercises for workout: ${workoutId}`);
|
||||||
|
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Found ${exercises.length} exercises for workout ${workoutId}`);
|
||||||
|
|
||||||
|
const result: WorkoutExercise[] = [];
|
||||||
|
|
||||||
|
for (const exercise of exercises) {
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Processing exercise: ${exercise.id}, exercise_id: ${exercise.exercise_id}`);
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Base exercise lookup result: ${baseExercise ? JSON.stringify(baseExercise) : 'null'}`);
|
||||||
|
|
||||||
|
// If base exercise not found, check if it exists in the exercises table
|
||||||
|
if (!baseExercise) {
|
||||||
|
const exerciseExists = await this.db.getFirstAsync<{ count: number }>(
|
||||||
|
`SELECT COUNT(*) as count FROM exercises WHERE id = ?`,
|
||||||
|
[exercise.exercise_id]
|
||||||
|
);
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Exercise ${exercise.exercise_id} exists in exercises table: ${(exerciseExists?.count ?? 0) > 0}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Found ${tags.length} tags for exercise ${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]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Found ${sets.length} sets for exercise ${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
|
||||||
|
}));
|
||||||
|
|
||||||
|
const exerciseTitle = baseExercise?.title || 'Unknown Exercise';
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Using title: ${exerciseTitle} for exercise ${exercise.id}`);
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
id: exercise.id,
|
||||||
|
exerciseId: exercise.exercise_id,
|
||||||
|
title: exerciseTitle,
|
||||||
|
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'] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[EnhancedWorkoutHistoryService] Returning ${result.length} processed exercises for workout ${workoutId}`);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[EnhancedWorkoutHistoryService] Error getting workout exercises:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user