diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bbccb..3a00c58 100644 --- a/CHANGELOG.md +++ b/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/), 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 ## Added diff --git a/app/(tabs)/history/_layout.tsx b/app/(tabs)/history/_layout.tsx index 59d5cd5..f16833c 100644 --- a/app/(tabs)/history/_layout.tsx +++ b/app/(tabs)/history/_layout.tsx @@ -17,7 +17,7 @@ export default function HistoryLayout() {
@@ -53,4 +53,4 @@ export default function HistoryLayout() { ); -} \ No newline at end of file +} diff --git a/app/(workout)/workout/[id].tsx b/app/(workout)/workout/[id].tsx new file mode 100644 index 0000000..e598fd6 --- /dev/null +++ b/app/(workout)/workout/[id].tsx @@ -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(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(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((_, 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 ( + + + + {isLoading ? ( + + + Loading workout details... + + ) : error ? ( + + Error + {error} + router.back()} + className="bg-primary px-4 py-2 rounded-md" + > + Go Back + + + ) : workout ? ( + + ) : ( + + Workout not found + + The workout you're looking for could not be found or may have been deleted. + + router.back()} + className="bg-primary px-4 py-2 rounded-md" + > + Go Back + + + )} + + ); +} \ No newline at end of file diff --git a/lib/db/services/EnhancedWorkoutHistoryService.ts b/lib/db/services/EnhancedWorkoutHistoryService.ts new file mode 100644 index 0000000..6e03168 --- /dev/null +++ b/lib/db/services/EnhancedWorkoutHistoryService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 []; + } +} +} \ No newline at end of file