diff --git a/app/(app)/(tabs)/(stats)/edit-history.tsx b/app/(app)/(tabs)/(stats)/edit-history.tsx index 62c38308..3faa1e75 100644 --- a/app/(app)/(tabs)/(stats)/edit-history.tsx +++ b/app/(app)/(tabs)/(stats)/edit-history.tsx @@ -10,6 +10,8 @@ import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery"; import { useEditCompletedWorkoutMutation } from "@/hooks/useEditCompletedWorkoutMutation"; import { ActivityIndicator } from "react-native-paper"; import { useCompletedWorkoutByIdQuery } from "@/hooks/useCompletedWorkoutByIdQuery"; +import { formatFromTotalSeconds, convertToTotalSeconds } from "@/utils/utility"; +import { TimeInput } from "@/components/TimeInput"; import Bugsnag from "@bugsnag/expo"; export default function EditCompletedWorkoutScreen() { @@ -36,8 +38,11 @@ export default function EditCompletedWorkoutScreen() { error: workoutError, } = useCompletedWorkoutByIdQuery(Number(id), weightUnit, distanceUnit); - const editWorkout = useEditCompletedWorkoutMutation(Number(id), weightUnit, distanceUnit); - + const editWorkout = useEditCompletedWorkoutMutation( + Number(id), + weightUnit, + distanceUnit, + ); // Update exercises when workout data is available useEffect(() => { if (workoutData) { @@ -58,10 +63,23 @@ export default function EditCompletedWorkoutScreen() { }, [exercises]); const handleSave = () => { - // Blur each input to ensure all onBlur events are fired Object.values(weightInputRefs.current).forEach((input) => input?.blur()); - editWorkout.mutate(exercises, { + // Merge weightInputs into exercises synchronously — onBlur state updates + // are batched by React and won't be applied before mutate runs. + const finalExercises = exercises.map((exercise, exerciseIndex) => ({ + ...exercise, + sets: exercise.sets.map((set, setIndex) => { + const key = `${exerciseIndex}-${setIndex}`; + if (weightInputs[key] === undefined) return set; + const raw = weightInputs[key]; + if (raw == null || raw.trim() === "") return { ...set, weight: null }; + const parsedWeight = parseFloat(raw); + return { ...set, weight: isNaN(parsedWeight) ? null : parsedWeight }; + }), + })); + + editWorkout.mutate(finalExercises, { onSuccess: () => { router.back(); }, @@ -90,13 +108,21 @@ export default function EditCompletedWorkoutScreen() { options={{ headerRight: () => ( - + {editWorkout.isPending ? ( + + ) : ( + + )} ), }} @@ -185,22 +211,18 @@ export default function EditCompletedWorkoutScreen() { ) : exercise.exercise_tracking_type === "time" ? ( - Time (Seconds) - { + Time (Min:Sec) + { setExercises((prev) => { const updated = [...prev]; updated[exerciseIndex].sets[setIndex].time = - Number(value); + convertToTotalSeconds(value); return updated; }); }} + style={styles.timeInput} /> ) : exercise.exercise_tracking_type === "reps" ? ( @@ -282,4 +304,14 @@ const styles = StyleSheet.create({ fontSize: 18, textAlign: "right", }, + timeInput: { + width: 96, + padding: 10, + borderColor: Colors.dark.subText, + borderWidth: 1, + borderRadius: 8, + color: Colors.dark.text, + fontSize: 18, + textAlign: "center", + }, }); diff --git a/app/(app)/(tabs)/(stats)/history-details.tsx b/app/(app)/(tabs)/(stats)/history-details.tsx index cde2b505..daae0741 100644 --- a/app/(app)/(tabs)/(stats)/history-details.tsx +++ b/app/(app)/(tabs)/(stats)/history-details.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo, useCallback } from "react"; +import { useState, useMemo, useCallback, useEffect } from "react"; import { View, StyleSheet, ScrollView, Alert } from "react-native"; import { Image } from "expo-image"; import { ThemedView } from "@/components/ThemedView"; @@ -11,14 +11,14 @@ import { useLocalSearchParams, useFocusEffect, } from "expo-router"; -import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery"; -import { fetchExerciseImagesByIds } from "@/utils/database"; import { byteArrayToBase64 } from "@/utils/utility"; import { parseISO, format } from "date-fns"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import { useSettingsQuery } from "@/hooks/useSettingsQuery"; -import { useCompletedWorkoutByIdQuery } from "@/hooks/useCompletedWorkoutByIdQuery"; +import { fetchCompletedWorkoutById } from "@/utils/database"; +import { CompletedWorkout } from "@/hooks/useCompletedWorkoutsQuery"; import { useDeleteCompletedWorkoutMutation } from "@/hooks/useDeleteCompletedWorkoutMutation"; +import { formatFromTotalSeconds } from "@/utils/utility"; import Bugsnag from "@bugsnag/expo"; const fallbackImage = require("@/assets/images/placeholder.webp"); @@ -26,6 +26,8 @@ const fallbackImage = require("@/assets/images/placeholder.webp"); export default function HistoryDetailsScreen() { const { id } = useLocalSearchParams(); const [workout, setWorkout] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const { data: settings, @@ -40,55 +42,50 @@ export default function HistoryDetailsScreen() { const countUnilateralDouble = settings?.countUnilateralDouble === "true"; const doubleWeightForPaired = settings?.doubleWeightForPaired === "true"; - const deleteMutation = useDeleteCompletedWorkoutMutation(); - - const { - data: workoutData, - isLoading: isWorkoutLoading, - error: workoutError, - refetch, - } = useCompletedWorkoutByIdQuery(Number(id), weightUnit, distanceUnit); - useEffect(() => { - if (workoutData) { - // Collect unique exercise IDs - const exerciseIds = workoutData.exercises.map( - (exercise) => exercise.exercise_id, - ); + if (settingsError instanceof Error) { + Bugsnag.notify(settingsError); + } + }, [settingsError]); - // Fetch images for these exercise IDs and attach them to the exercises - const fetchImages = async () => { - try { - const imagesMap = await fetchExerciseImagesByIds(exerciseIds); + const deleteMutation = useDeleteCompletedWorkoutMutation(); - // Attach images to exercises - const exercisesWithImages = workoutData.exercises.map((exercise) => ({ - ...exercise, - exercise_image: imagesMap[exercise.exercise_id], - })); + useFocusEffect( + useCallback(() => { + const numId = Number(Array.isArray(id) ? id[0] : id); + if (!numId) { + setIsLoading(false); + setWorkout(null); + return; + } - // Update workout data with exercises including images - setWorkout({ - ...workoutData, - exercises: exercisesWithImages, - }); - } catch (error: any) { - console.error("Error fetching exercise images:", error); - Bugsnag.notify(error); - } - }; + let cancelled = false; + setIsLoading(true); + setError(null); - fetchImages(); - } - }, [workoutData]); + fetchCompletedWorkoutById(numId, weightUnit, distanceUnit) + .then((data) => { + if (!cancelled) { + setWorkout(data); + setError(null); + setIsLoading(false); + } + }) + .catch((err) => { + if (!cancelled) { + setError(err instanceof Error ? err : new Error(String(err))); + setWorkout(null); + setIsLoading(false); + Bugsnag.notify(err); + } + }); - useFocusEffect( - useCallback(() => { - refetch(); - }, [refetch]), + return () => { + cancelled = true; + }; + }, [id, weightUnit, distanceUnit]), ); - // Calculate total volume const totalVolume = useMemo(() => { if (!workout) return 0; @@ -106,9 +103,15 @@ export default function HistoryDetailsScreen() { return parseFloat((exerciseAcc + exerciseVolume).toFixed(1)); }, 0); - }, [workout, bodyWeight, excludeWarmup, countUnilateralDouble, doubleWeightForPaired]); + }, [ + workout, + bodyWeight, + excludeWarmup, + countUnilateralDouble, + doubleWeightForPaired, + ]); - if (isWorkoutLoading || !workout || settingsLoading) { + if (isLoading || settingsLoading) { return ( @@ -116,12 +119,16 @@ export default function HistoryDetailsScreen() { ); } - if (settingsError || workoutError) { - const error = settingsError || workoutError; - if (error instanceof Error) { - Bugsnag.notify(error); - return Error: {error.message}; - } + if (settingsError instanceof Error) { + return Error: {settingsError.message}; + } + + if (error) { + return Error: {error.message}; + } + + if (!workout) { + return null; } const isoDateString = workout.date_completed.replace(" ", "T"); @@ -219,10 +226,8 @@ export default function HistoryDetailsScreen() { {/* Exercise List */} {workout.exercises.map((exercise) => { - // Check if the image exists let imageUri = ""; if (exercise.exercise_image) { - // Convert the image blob to Base64 const base64Image = byteArrayToBase64(exercise.exercise_image); imageUri = `data:image/webp;base64,${base64Image}`; } @@ -253,7 +258,9 @@ export default function HistoryDetailsScreen() { {exercise.exercise_tracking_type === "time" ? ( - {set.time} Seconds + {set.time != null + ? formatFromTotalSeconds(set.time) + : "—"} ) : exercise.exercise_tracking_type === "reps" ? ( diff --git a/app/(app)/(workout)/index.tsx b/app/(app)/(workout)/index.tsx index 06e11433..8a5c325d 100644 --- a/app/(app)/(workout)/index.tsx +++ b/app/(app)/(workout)/index.tsx @@ -33,6 +33,7 @@ import { linkCompletedWorkoutToWorkout, } from "@/utils/database"; import { cancelRestNotifications } from "@/utils/restNotification"; +import { convertTimeStrToSeconds } from "@/utils/utility"; import { useQueryClient } from "@tanstack/react-query"; import { UserExercise, Workout } from "@/store/workoutStore"; @@ -518,7 +519,7 @@ export default function WorkoutOverviewScreen() { set_number: parseInt(setIndex) + 1, weight: set.weight ? parseFloat(set.weight) : null, reps: set.reps ? parseInt(set.reps) : null, - time: set.time ? parseInt(set.time) : null, + time: set.time ? convertTimeStrToSeconds(set.time) : null, distance: set.distance !== "" && set.distance != null ? parseFloat(set.distance) diff --git a/app/(app)/(workout)/workout-session.tsx b/app/(app)/(workout)/workout-session.tsx index d7058d7e..c38f22fd 100644 --- a/app/(app)/(workout)/workout-session.tsx +++ b/app/(app)/(workout)/workout-session.tsx @@ -632,6 +632,17 @@ export default function WorkoutSessionScreen() { style: "destructive", onPress: () => { removeSet(index); + const st = useActiveWorkoutStore.getState(); + const newExerciseIndex = st.currentExerciseIndex; + const newSetIndex = st.currentSetIndices[newExerciseIndex] ?? 0; + setSlots((prev) => { + const u = [...prev] as [SlotData, SlotData, SlotData]; + u[currentSlotIndex] = { + exerciseIndex: newExerciseIndex, + setIndex: newSetIndex, + }; + return u; + }); }, }, ]); diff --git a/components/SessionSetInfo.tsx b/components/SessionSetInfo.tsx index 0369377e..1e33f384 100644 --- a/components/SessionSetInfo.tsx +++ b/components/SessionSetInfo.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useCallback } from "react"; import { View, TextInput, StyleSheet } from "react-native"; import { IconButton, @@ -107,7 +107,6 @@ export default function SessionSetInfo({ onToggleSetType, }: SessionSetInfoProps) { const [menuVisible, setMenuVisible] = useState(false); - const [timeInput, setTimeInput] = useState(time); const [timerModalVisible, setTimerModalVisible] = useState(false); const weightMinusPress = useContinuousPress( @@ -135,13 +134,7 @@ export default function SessionSetInfo({ useCallback(() => handleDistanceChange(1), [handleDistanceChange]), ); - // Update timeInput when time prop changes - useEffect(() => { - setTimeInput(time); - }, [time]); - - // Format display value - now always formatted - const displayValue = formatTimeInput(timeInput); + const displayValue = formatTimeInput(time); const openMenu = () => setMenuVisible(true); const closeMenu = () => setMenuVisible(false); @@ -403,6 +396,7 @@ export default function SessionSetInfo({