Fix history tab navigation issues and improve workout detail error handling

This commit is contained in:
DocNR 2025-03-23 06:59:22 -04:00
parent e4aa59a07e
commit 3004bcd4f8
4 changed files with 1025 additions and 3 deletions

View File

@ -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

View File

@ -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' }}
/> />

View 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>
);
}

View 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 [];
}
}
}