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