diff --git a/App.tsx b/App.tsx index e3b1222..ddb0e2a 100644 --- a/App.tsx +++ b/App.tsx @@ -8,9 +8,8 @@ import AICoach from './components/AICoach'; import Plans from './components/Plans'; import Login from './components/Login'; import Profile from './components/Profile'; -import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language, SporadicSet } from './types'; +import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types'; import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage'; -import { getSporadicSets, updateSporadicSet, deleteSporadicSet } from './services/sporadicSets'; import { getCurrentUserProfile, getMe } from './services/auth'; import { getSystemLanguage } from './services/i18n'; import { logWeight } from './services/weight'; @@ -25,7 +24,7 @@ function App() { const [plans, setPlans] = useState([]); const [activeSession, setActiveSession] = useState(null); const [activePlan, setActivePlan] = useState(null); - const [sporadicSets, setSporadicSets] = useState([]); + useEffect(() => { // Set initial language @@ -68,13 +67,11 @@ function App() { // Load plans const p = await getPlans(currentUser.id); setPlans(p); - // Load sporadic sets - const sporadicSets = await getSporadicSets(); - setSporadicSets(sporadicSets); + } else { setSessions([]); setPlans([]); - setSporadicSets([]); + } }; loadSessions(); @@ -112,6 +109,7 @@ function App() { const newSession: WorkoutSession = { id: generateId(), startTime: Date.now(), + type: 'STANDARD', userBodyWeight: currentWeight, sets: [], planId: plan?.id, @@ -198,24 +196,7 @@ function App() { setSessions(prev => prev.filter(s => s.id !== sessionId)); }; - const handleSporadicSetAdded = async () => { - const sets = await getSporadicSets(); - setSporadicSets(sets); - }; - const handleUpdateSporadicSet = async (set: SporadicSet) => { - const updated = await updateSporadicSet(set.id, set); - if (updated) { - setSporadicSets(prev => prev.map(s => s.id === set.id ? updated : s)); - } - }; - - const handleDeleteSporadicSet = async (id: string) => { - const success = await deleteSporadicSet(id); - if (success) { - setSporadicSets(prev => prev.filter(s => s.id !== id)); - } - }; if (!currentUser) { return ; @@ -236,14 +217,12 @@ function App() { userWeight={currentUser.profile?.weight} activeSession={activeSession} activePlan={activePlan} - sporadicSets={sporadicSets} onSessionStart={handleStartSession} onSessionEnd={handleEndSession} onSessionQuit={handleQuitSession} onSetAdded={handleAddSet} onRemoveSet={handleRemoveSetFromActive} onUpdateSet={handleUpdateSetInActive} - onSporadicSetAdded={handleSporadicSetAdded} lang={language} /> )} @@ -253,11 +232,8 @@ function App() { {currentTab === 'HISTORY' && ( )} diff --git a/components/History.tsx b/components/History.tsx index 0d2715c..ba6c57a 100644 --- a/components/History.tsx +++ b/components/History.tsx @@ -1,24 +1,20 @@ import React, { useState } from 'react'; import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react'; -import { WorkoutSession, ExerciseType, WorkoutSet, Language, SporadicSet } from '../types'; +import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types'; import { t } from '../services/i18n'; interface HistoryProps { sessions: WorkoutSession[]; - sporadicSets?: SporadicSet[]; onUpdateSession?: (session: WorkoutSession) => void; onDeleteSession?: (sessionId: string) => void; - onUpdateSporadicSet?: (set: SporadicSet) => void; - onDeleteSporadicSet?: (setId: string) => void; lang: Language; } -const History: React.FC = ({ sessions, sporadicSets, onUpdateSession, onDeleteSession, onUpdateSporadicSet, onDeleteSporadicSet, lang }) => { +const History: React.FC = ({ sessions, onUpdateSession, onDeleteSession, lang }) => { const [editingSession, setEditingSession] = useState(null); const [deletingId, setDeletingId] = useState(null); - const [editingSporadicSet, setEditingSporadicSet] = useState(null); - const [deletingSporadicId, setDeletingSporadicId] = useState(null); + const calculateSessionWork = (session: WorkoutSession) => { const bw = session.userBodyWeight || 70; @@ -93,26 +89,9 @@ const History: React.FC = ({ sessions, sporadicSets, onUpdateSessi } } - const handleSaveSporadicEdit = () => { - if (editingSporadicSet && onUpdateSporadicSet) { - onUpdateSporadicSet(editingSporadicSet); - setEditingSporadicSet(null); - } - }; - const handleUpdateSporadicField = (field: keyof SporadicSet, value: number) => { - if (!editingSporadicSet) return; - setEditingSporadicSet({ ...editingSporadicSet, [field]: value }); - }; - const handleConfirmDeleteSporadic = () => { - if (deletingSporadicId && onDeleteSporadicSet) { - onDeleteSporadicSet(deletingSporadicId); - setDeletingSporadicId(null); - } - }; - - if (sessions.length === 0 && (!sporadicSets || sporadicSets.length === 0)) { + if (sessions.length === 0) { return (
@@ -128,7 +107,8 @@ const History: React.FC = ({ sessions, sporadicSets, onUpdateSessi
- {sessions.map((session) => { + {/* Regular Workout Sessions */} + {sessions.filter(s => s.type === 'STANDARD').map((session) => { const totalWork = calculateSessionWork(session); return ( @@ -198,30 +178,35 @@ const History: React.FC = ({ sessions, sporadicSets, onUpdateSessi ) })} - {/* Sporadic Sets Section */} - {sporadicSets && sporadicSets.length > 0 && ( + {/* Quick Log Sessions */} + {sessions.filter(s => s.type === 'QUICK_LOG').length > 0 && (
-

{t('sporadic_sets_title', lang)}

+

{t('quick_log', lang)}

{Object.entries( - sporadicSets.reduce((groups: Record, set) => { - const date = new Date(set.timestamp).toISOString().split('T')[0]; - if (!groups[date]) groups[date] = []; - groups[date].push(set); - return groups; - }, {}) + sessions + .filter(s => s.type === 'QUICK_LOG') + .reduce((groups: Record, session) => { + const date = new Date(session.startTime).toISOString().split('T')[0]; + if (!groups[date]) groups[date] = []; + groups[date].push(session); + return groups; + }, {}) ) .sort(([a], [b]) => b.localeCompare(a)) - .map(([date, sets]) => ( + .map(([date, daySessions]) => (
{date}
- {(sets as SporadicSet[]).map(set => ( + {daySessions.flatMap(session => session.sets).map((set, idx) => (
-
{set.exerciseName}
+
+ {set.exerciseName} + {set.side && {t(set.side.toLowerCase() as any, lang)}} +
{set.type === ExerciseType.STRENGTH && `${set.weight || 0}kg x ${set.reps || 0}`} {set.type === ExerciseType.BODYWEIGHT && `${set.weight ? `+${set.weight}kg` : 'BW'} x ${set.reps || 0}`} @@ -237,13 +222,26 @@ const History: React.FC = ({ sessions, sporadicSets, onUpdateSessi
)} +
{/* DELETE CONFIRMATION DIALOG (MD3) */} @@ -336,7 +335,7 @@ const History: React.FC = ({ sessions, sporadicSets, onUpdateSessi
{idx + 1} - {set.exerciseName} + {set.exerciseName}{set.side && {t(set.side.toLowerCase(), lang)}}
)} - {/* Sporadic Set Delete Confirmation */} - {deletingSporadicId && ( -
-
-

{t('delete', lang)}

-

{t('delete_confirm', lang)}

-
- - -
-
-
- )} +
); }; diff --git a/components/Tracker/ActiveSessionView.tsx b/components/Tracker/ActiveSessionView.tsx index 2a302ff..34f32f4 100644 --- a/components/Tracker/ActiveSessionView.tsx +++ b/components/Tracker/ActiveSessionView.tsx @@ -5,6 +5,7 @@ import { t } from '../../services/i18n'; import FilledInput from '../FilledInput'; import ExerciseModal from '../ExerciseModal'; import { useTracker } from './useTracker'; +import SetLogger from './SetLogger'; interface ActiveSessionViewProps { tracker: ReturnType; @@ -178,237 +179,11 @@ const ActiveSessionView: React.FC = ({ tracker, activeSe
-
- ) => { - setSearchQuery(e.target.value); - setShowSuggestions(true); - }} - onFocus={() => { - setSearchQuery(''); - setShowSuggestions(true); - }} - onBlur={() => setTimeout(() => setShowSuggestions(false), 100)} // Delay hiding to allow click - icon={} - autoComplete="off" - type="text" - /> - - {showSuggestions && ( -
- {filteredExercises.length > 0 ? ( - filteredExercises.map(ex => ( - - )) - ) : ( -
{t('no_exercises_found', lang)}
- )} -
- )} -
- - {selectedExercise && ( -
- {/* Unilateral Exercise Toggle */} - {selectedExercise.isUnilateral && ( -
- tracker.handleToggleSameValues(e.target.checked)} - className="w-5 h-5 rounded border-2 border-outline bg-surface-container-high checked:bg-primary checked:border-primary cursor-pointer" - /> - -
- )} - - {/* Input Forms */} - {selectedExercise.isUnilateral && !tracker.sameValuesBothSides ? ( - /* Separate Left/Right Inputs */ -
- {/* Left Side */} -
-
- L - {t('left', lang)} -
-
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setWeightLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - tracker.setRepsLeft(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setDurationLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - tracker.setDistanceLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - tracker.setHeightLeft(e.target.value)} - icon={} - /> - )} -
-
- - {/* Right Side */} -
-
- R - {t('right', lang)} -
-
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setWeightRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - tracker.setRepsRight(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setDurationRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - tracker.setDistanceRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - tracker.setHeightRight(e.target.value)} - icon={} - /> - )} -
-
-
- ) : ( - /* Single Input Form (for bilateral or unilateral with same values) */ -
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - setWeight(e.target.value)} - icon={} - autoFocus={activePlan && !isPlanFinished && activePlan.steps[currentStepIndex]?.isWeighted && (selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STRENGTH)} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - setReps(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - setDuration(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - setDistance(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - setHeight(e.target.value)} - icon={} - /> - )} -
- )} - - -
- )} + {activeSession.sets.length > 0 && (
@@ -425,7 +200,7 @@ const ActiveSessionView: React.FC = ({ tracker, activeSe
{isEditing ? (
-
{set.exerciseName}
+
{set.exerciseName}{set.side && {t(set.side.toLowerCase() as any, lang)}}
{set.weight !== undefined && ( = ({ tracker, activeSe
) : (
-
{set.exerciseName}
+
{set.exerciseName}{set.side && {t(set.side.toLowerCase() as any, lang)}}
{set.type === ExerciseType.STRENGTH && `${set.weight ? `${set.weight}kg` : ''} ${set.reps ? `x ${set.reps}` : ''}`.trim() diff --git a/components/Tracker/SetLogger.tsx b/components/Tracker/SetLogger.tsx new file mode 100644 index 0000000..126c26a --- /dev/null +++ b/components/Tracker/SetLogger.tsx @@ -0,0 +1,179 @@ +import React from 'react'; +import { Dumbbell, Scale, Activity, Timer as TimerIcon, ArrowRight, ArrowUp, Plus, CheckCircle } from 'lucide-react'; +import { ExerciseType, Language } from '../../types'; +import { t } from '../../services/i18n'; +import FilledInput from '../FilledInput'; +import { useTracker } from './useTracker'; + +interface SetLoggerProps { + tracker: ReturnType; + lang: Language; + onLogSet: () => void; + isSporadic?: boolean; +} + +const SetLogger: React.FC = ({ tracker, lang, onLogSet, isSporadic = false }) => { + const { + searchQuery, + setSearchQuery, + setShowSuggestions, + showSuggestions, + filteredExercises, + setSelectedExercise, + selectedExercise, + weight, + setWeight, + reps, + setReps, + duration, + setDuration, + distance, + setDistance, + height, + setHeight, + setIsCreating, + sporadicSuccess, + activePlan, + currentStepIndex, + unilateralSide, + setUnilateralSide + } = tracker; + + const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length; + + return ( +
+ {/* Exercise Selection */} +
+ ) => { + setSearchQuery(e.target.value); + setShowSuggestions(true); + }} + onFocus={() => { + setSearchQuery(''); + setShowSuggestions(true); + }} + onBlur={() => setTimeout(() => setShowSuggestions(false), 100)} + icon={} + autoComplete="off" + type="text" + /> + + {showSuggestions && ( +
+ {filteredExercises.length > 0 ? ( + filteredExercises.map(ex => ( + + )) + ) : ( +
{t('no_exercises_found', lang)}
+ )} +
+ )} +
+ + {selectedExercise && ( +
+ {/* Unilateral Exercise Toggle */} + {selectedExercise.isUnilateral && ( +
+ + +
+ )} + + {/* Input Forms */} +
+ {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( + setWeight(e.target.value)} + icon={} + autoFocus={!isSporadic && activePlan && !isPlanFinished && activePlan.steps[currentStepIndex]?.isWeighted && (selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STRENGTH)} + /> + )} + {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( + setReps(e.target.value)} + icon={} + type="number" + /> + )} + {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( + setDuration(e.target.value)} + icon={} + /> + )} + {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( + setDistance(e.target.value)} + icon={} + /> + )} + {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( + setHeight(e.target.value)} + icon={} + /> + )} +
+ + +
+ )} +
+ ); +}; + +export default SetLogger; diff --git a/components/Tracker/SporadicView.tsx b/components/Tracker/SporadicView.tsx index 731c694..46f530d 100644 --- a/components/Tracker/SporadicView.tsx +++ b/components/Tracker/SporadicView.tsx @@ -1,58 +1,45 @@ import React, { useState, useEffect } from 'react'; -import { Dumbbell, Scale, Activity, Timer as TimerIcon, ArrowRight, ArrowUp, Plus, CheckCircle, Edit, Trash2 } from 'lucide-react'; -import { ExerciseType, Language, SporadicSet } from '../../types'; +import { CheckCircle, Plus, Pencil, Trash2, X, Save } from 'lucide-react'; +import { Language, WorkoutSet } from '../../types'; import { t } from '../../services/i18n'; -import FilledInput from '../FilledInput'; import ExerciseModal from '../ExerciseModal'; import { useTracker } from './useTracker'; +import SetLogger from './SetLogger'; interface SporadicViewProps { tracker: ReturnType; lang: Language; - sporadicSets?: SporadicSet[]; } -const SporadicView: React.FC = ({ tracker, lang, sporadicSets }) => { +const SporadicView: React.FC = ({ tracker, lang }) => { const { - searchQuery, - setSearchQuery, - setShowSuggestions, - showSuggestions, - filteredExercises, - setSelectedExercise, - selectedExercise, - weight, - setWeight, - reps, - setReps, - duration, - setDuration, - distance, - setDistance, - height, - setHeight, handleLogSporadicSet, - sporadicSuccess, setIsSporadicMode, isCreating, setIsCreating, handleCreateExercise, exercises, - resetForm + resetForm, + quickLogSession, + selectedExercise, + loadQuickLogSession } = tracker; - const [todaysSets, setTodaysSets] = useState([]); + const [todaysSets, setTodaysSets] = useState([]); + const [editingSetId, setEditingSetId] = useState(null); + const [editingSet, setEditingSet] = useState(null); + const [deletingSetId, setDeletingSetId] = useState(null); useEffect(() => { - if (sporadicSets) { - const startOfDay = new Date(); - startOfDay.setHours(0, 0, 0, 0); - const todayS = sporadicSets.filter(s => s.timestamp >= startOfDay.getTime()); - setTodaysSets(todayS.sort((a, b) => b.timestamp - a.timestamp)); + if (quickLogSession && quickLogSession.sets) { + // Sets are already ordered by timestamp desc in the backend query, but let's ensure + setTodaysSets([...quickLogSession.sets].sort((a, b) => b.timestamp - a.timestamp)); + } else { + setTodaysSets([]); } - }, [sporadicSets]); + }, [quickLogSession]); - const renderSetMetrics = (set: SporadicSet) => { + const renderSetMetrics = (set: WorkoutSet) => { const metrics: string[] = []; if (set.weight) metrics.push(`${set.weight} ${t('weight_kg', lang)}`); if (set.reps) metrics.push(`${set.reps} ${t('reps', lang)}`); @@ -93,254 +80,45 @@ const SporadicView: React.FC = ({ tracker, lang, sporadicSets
- {/* Exercise Selection */} -
- ) => { - setSearchQuery(e.target.value); - setShowSuggestions(true); - }} - onFocus={() => { - setSearchQuery(''); - setShowSuggestions(true); - }} - onBlur={() => setTimeout(() => setShowSuggestions(false), 100)} - icon={} - autoComplete="off" - type="text" - /> - - {showSuggestions && ( -
- {filteredExercises.length > 0 ? ( - filteredExercises.map(ex => ( - - )) - ) : ( -
{t('no_exercises_found', lang)}
- )} -
- )} -
- - {selectedExercise && ( -
- {/* Unilateral Exercise Toggle */} - {selectedExercise.isUnilateral && ( -
- tracker.handleToggleSameValues(e.target.checked)} - className="w-5 h-5 rounded border-2 border-outline bg-surface-container-high checked:bg-primary checked:border-primary cursor-pointer" - /> - -
- )} - - {/* Input Forms */} - {selectedExercise.isUnilateral && !tracker.sameValuesBothSides ? ( - /* Separate Left/Right Inputs */ -
- {/* Left Side */} -
-
- L - {t('left', lang)} -
-
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setWeightLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - tracker.setRepsLeft(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setDurationLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - tracker.setDistanceLeft(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - tracker.setHeightLeft(e.target.value)} - icon={} - /> - )} -
-
- - {/* Right Side */} -
-
- R - {t('right', lang)} -
-
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setWeightRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - tracker.setRepsRight(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - tracker.setDurationRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - tracker.setDistanceRight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - tracker.setHeightRight(e.target.value)} - icon={} - /> - )} -
-
-
- ) : ( - /* Single Input Form (for bilateral or unilateral with same values) */ -
- {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.STATIC) && ( - setWeight(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.STRENGTH || selectedExercise.type === ExerciseType.BODYWEIGHT || selectedExercise.type === ExerciseType.PLYOMETRIC) && ( - setReps(e.target.value)} - icon={} - type="number" - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.STATIC) && ( - setDuration(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.CARDIO || selectedExercise.type === ExerciseType.LONG_JUMP) && ( - setDistance(e.target.value)} - icon={} - /> - )} - {(selectedExercise.type === ExerciseType.HIGH_JUMP) && ( - setHeight(e.target.value)} - icon={} - /> - )} -
- )} - - -
- )} + {/* History Section */} {todaysSets.length > 0 && (

{t('history_section', lang)}

- {todaysSets.map(set => ( + {todaysSets.map((set, idx) => (
-
-

{set.exerciseName}

-

{renderSetMetrics(set)}

+
+
+ {todaysSets.length - idx} +
+
+

{set.exerciseName}{set.side && {t(set.side.toLowerCase() as any, lang)}}

+

{renderSetMetrics(set)}

+
- {/* Edit and Delete buttons can be added here in the future */} + +
))} @@ -358,6 +136,149 @@ const SporadicView: React.FC = ({ tracker, lang, sporadicSets existingExercises={exercises} /> )} + + {/* Edit Set Modal */} + {editingSetId && editingSet && ( +
+
+
+

{t('edit', lang)}

+ +
+
+ {(editingSet.type === 'STRENGTH' || editingSet.type === 'BODYWEIGHT') && ( + <> +
+ + setEditingSet({ ...editingSet, weight: parseFloat(e.target.value) || 0 })} + className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+
+ + setEditingSet({ ...editingSet, reps: parseInt(e.target.value) || 0 })} + className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ + )} + {(editingSet.type === 'CARDIO' || editingSet.type === 'STATIC') && ( +
+ + setEditingSet({ ...editingSet, durationSeconds: parseInt(e.target.value) || 0 })} + className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ )} + {editingSet.type === 'CARDIO' && ( +
+ + setEditingSet({ ...editingSet, distanceMeters: parseFloat(e.target.value) || 0 })} + className="w-full mt-1 px-4 py-2 bg-surface-container-high text-on-surface rounded-lg focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ )} +
+
+ + +
+
+
+ )} + + {/* Delete Confirmation Modal */} + {deletingSetId && ( +
+
+

{t('delete', lang)}

+

{t('delete_confirm', lang)}

+
+ + +
+
+
+ )}
); }; diff --git a/components/Tracker/index.tsx b/components/Tracker/index.tsx index a5f9572..8e81aa2 100644 --- a/components/Tracker/index.tsx +++ b/components/Tracker/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { WorkoutSession, WorkoutSet, WorkoutPlan, Language, SporadicSet } from '../../types'; +import { WorkoutSession, WorkoutSet, WorkoutPlan, Language } from '../../types'; import { useTracker } from './useTracker'; import IdleView from './IdleView'; import SporadicView from './SporadicView'; @@ -11,7 +11,6 @@ interface TrackerProps { userWeight?: number; activeSession: WorkoutSession | null; activePlan: WorkoutPlan | null; - sporadicSets?: SporadicSet[]; onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void; onSessionEnd: () => void; onSessionQuit: () => void; @@ -25,7 +24,7 @@ interface TrackerProps { const Tracker: React.FC = (props) => { const tracker = useTracker(props); const { isSporadicMode } = tracker; - const { activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet, sporadicSets } = props; + const { activeSession, lang, onSessionEnd, onSessionQuit, onRemoveSet } = props; if (activeSession) { return ( @@ -41,7 +40,7 @@ const Tracker: React.FC = (props) => { } if (isSporadicMode) { - return ; + return ; } return ; diff --git a/components/Tracker/useTracker.ts b/components/Tracker/useTracker.ts index a71d2d7..56e7b79 100644 --- a/components/Tracker/useTracker.ts +++ b/components/Tracker/useTracker.ts @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { WorkoutSession, WorkoutSet, ExerciseDef, ExerciseType, WorkoutPlan, Language } from '../../types'; import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../../services/storage'; import { api } from '../../services/api'; -import { logSporadicSet } from '../../services/sporadicSets'; + interface UseTrackerProps { userId: string; @@ -73,39 +73,13 @@ export const useTracker = ({ const [editDistance, setEditDistance] = useState(''); const [editHeight, setEditHeight] = useState(''); - // Sporadic Set State + // Quick Log State + const [quickLogSession, setQuickLogSession] = useState(null); const [isSporadicMode, setIsSporadicMode] = useState(false); const [sporadicSuccess, setSporadicSuccess] = useState(false); // Unilateral Exercise State - const [sameValuesBothSides, setSameValuesBothSides] = useState(true); - const [weightLeft, setWeightLeft] = useState(''); - const [weightRight, setWeightRight] = useState(''); - const [repsLeft, setRepsLeft] = useState(''); - const [repsRight, setRepsRight] = useState(''); - const [durationLeft, setDurationLeft] = useState(''); - const [durationRight, setDurationRight] = useState(''); - const [distanceLeft, setDistanceLeft] = useState(''); - const [distanceRight, setDistanceRight] = useState(''); - const [heightLeft, setHeightLeft] = useState(''); - const [heightRight, setHeightRight] = useState(''); - - const handleToggleSameValues = (checked: boolean) => { - setSameValuesBothSides(checked); - if (!checked) { - // Propagate values from single fields to left/right fields - setWeightLeft(weight); - setWeightRight(weight); - setRepsLeft(reps); - setRepsRight(reps); - setDurationLeft(duration); - setDurationRight(duration); - setDistanceLeft(distance); - setDistanceRight(distance); - setHeightLeft(height); - setHeightRight(height); - } - }; + const [unilateralSide, setUnilateralSide] = useState<'LEFT' | 'RIGHT'>('LEFT'); useEffect(() => { const loadData = async () => { @@ -120,10 +94,34 @@ export const useTracker = ({ } else if (userWeight) { setUserBodyWeight(userWeight.toString()); } + + // Load Quick Log Session + try { + const response = await api.get('/sessions/quick-log'); + if (response.success && response.session) { + setQuickLogSession(response.session); + } + } catch (error) { + console.error("Failed to load quick log session:", error); + } }; loadData(); }, [activeSession, userId, userWeight, activePlan]); + // Function to reload Quick Log session + const loadQuickLogSession = async () => { + try { + const response = await api.get('/sessions/quick-log'); + if (response.success && response.session) { + setQuickLogSession(response.session); + } + } catch (error) { + console.error("Failed to load quick log session:", error); + } + }; + + + // Timer Logic useEffect(() => { let interval: number; @@ -167,7 +165,6 @@ export const useTracker = ({ } }, [activeSession, activePlan]); - useEffect(() => { if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) { if (currentStepIndex < activePlan.steps.length) { @@ -222,6 +219,7 @@ export const useTracker = ({ updateSelection(); }, [selectedExercise, userId]); + const filteredExercises = searchQuery === '' ? exercises : exercises.filter(ex => @@ -246,363 +244,126 @@ export const useTracker = ({ const handleAddSet = async () => { if (!activeSession || !selectedExercise) return; - // For unilateral exercises, create two sets (LEFT and RIGHT) + const setData: Partial = { + exerciseId: selectedExercise.id, + }; + if (selectedExercise.isUnilateral) { - const setsToCreate: Array & { side: 'LEFT' | 'RIGHT' }> = []; + setData.side = unilateralSide; + } - if (sameValuesBothSides) { - // Create two identical sets with LEFT and RIGHT sides - const setData: Partial = { - exerciseId: selectedExercise.id, - }; + switch (selectedExercise.type) { + case ExerciseType.STRENGTH: + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); + break; + case ExerciseType.BODYWEIGHT: + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + break; + case ExerciseType.CARDIO: + if (duration) setData.durationSeconds = parseInt(duration); + if (distance) setData.distanceMeters = parseFloat(distance); + break; + case ExerciseType.STATIC: + if (duration) setData.durationSeconds = parseInt(duration); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + break; + case ExerciseType.HIGH_JUMP: + if (height) setData.height = parseFloat(height); + break; + case ExerciseType.LONG_JUMP: + if (distance) setData.distanceMeters = parseFloat(distance); + break; + case ExerciseType.PLYOMETRIC: + if (reps) setData.reps = parseInt(reps); + break; + } - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weight) setData.weight = parseFloat(weight); - if (reps) setData.reps = parseInt(reps); - break; - case ExerciseType.BODYWEIGHT: - if (weight) setData.weight = parseFloat(weight); - if (reps) setData.reps = parseInt(reps); - setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (duration) setData.durationSeconds = parseInt(duration); - if (distance) setData.distanceMeters = parseFloat(distance); - break; - case ExerciseType.STATIC: - if (duration) setData.durationSeconds = parseInt(duration); - setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (height) setData.height = parseFloat(height); - break; - case ExerciseType.LONG_JUMP: - if (distance) setData.distanceMeters = parseFloat(distance); - break; - case ExerciseType.PLYOMETRIC: - if (reps) setData.reps = parseInt(reps); - break; - } + try { + const response = await api.post('/sessions/active/log-set', setData); + if (response.success) { + const { newSet, activeExerciseId } = response; + onSetAdded(newSet); - setsToCreate.push({ ...setData, side: 'LEFT' }); - setsToCreate.push({ ...setData, side: 'RIGHT' }); - } else { - // Create separate sets for LEFT and RIGHT with different values - const leftSetData: Partial = { - exerciseId: selectedExercise.id, - }; - const rightSetData: Partial = { - exerciseId: selectedExercise.id, - }; - - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weightLeft) leftSetData.weight = parseFloat(weightLeft); - if (repsLeft) leftSetData.reps = parseInt(repsLeft); - if (weightRight) rightSetData.weight = parseFloat(weightRight); - if (repsRight) rightSetData.reps = parseInt(repsRight); - break; - case ExerciseType.BODYWEIGHT: - if (weightLeft) leftSetData.weight = parseFloat(weightLeft); - if (repsLeft) leftSetData.reps = parseInt(repsLeft); - leftSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - if (weightRight) rightSetData.weight = parseFloat(weightRight); - if (repsRight) rightSetData.reps = parseInt(repsRight); - rightSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (durationLeft) leftSetData.durationSeconds = parseInt(durationLeft); - if (distanceLeft) leftSetData.distanceMeters = parseFloat(distanceLeft); - if (durationRight) rightSetData.durationSeconds = parseInt(durationRight); - if (distanceRight) rightSetData.distanceMeters = parseFloat(distanceRight); - break; - case ExerciseType.STATIC: - if (durationLeft) leftSetData.durationSeconds = parseInt(durationLeft); - leftSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - if (durationRight) rightSetData.durationSeconds = parseInt(durationRight); - rightSetData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (heightLeft) leftSetData.height = parseFloat(heightLeft); - if (heightRight) rightSetData.height = parseFloat(heightRight); - break; - case ExerciseType.LONG_JUMP: - if (distanceLeft) leftSetData.distanceMeters = parseFloat(distanceLeft); - if (distanceRight) rightSetData.distanceMeters = parseFloat(distanceRight); - break; - case ExerciseType.PLYOMETRIC: - if (repsLeft) leftSetData.reps = parseInt(repsLeft); - if (repsRight) rightSetData.reps = parseInt(repsRight); - break; - } - - setsToCreate.push({ ...leftSetData, side: 'LEFT' }); - setsToCreate.push({ ...rightSetData, side: 'RIGHT' }); - } - - // Log both sets - try { - for (const setData of setsToCreate) { - const response = await api.post('/sessions/active/log-set', setData); - if (response.success) { - const { newSet } = response; - onSetAdded(newSet); + if (activePlan && activeExerciseId) { + const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId); + if (nextStepIndex !== -1) { + setCurrentStepIndex(nextStepIndex); } + } else if (activePlan && !activeExerciseId) { + // Plan is finished + setCurrentStepIndex(activePlan.steps.length); } - - // Update plan progress after logging both sets - if (activePlan) { - const response = await api.post('/sessions/active/log-set', { exerciseId: selectedExercise.id }); - if (response.success && response.activeExerciseId) { - const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === response.activeExerciseId); - if (nextStepIndex !== -1) { - setCurrentStepIndex(nextStepIndex); - } - } else if (response.success && !response.activeExerciseId) { - setCurrentStepIndex(activePlan.steps.length); - } - } - } catch (error) { - console.error("Failed to log unilateral sets:", error); - } - } else { - // Regular bilateral exercise - single set - const setData: Partial = { - exerciseId: selectedExercise.id, - }; - - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weight) setData.weight = parseFloat(weight); - if (reps) setData.reps = parseInt(reps); - break; - case ExerciseType.BODYWEIGHT: - if (weight) setData.weight = parseFloat(weight); - if (reps) setData.reps = parseInt(reps); - setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (duration) setData.durationSeconds = parseInt(duration); - if (distance) setData.distanceMeters = parseFloat(distance); - break; - case ExerciseType.STATIC: - if (duration) setData.durationSeconds = parseInt(duration); - setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (height) setData.height = parseFloat(height); - break; - case ExerciseType.LONG_JUMP: - if (distance) setData.distanceMeters = parseFloat(distance); - break; - case ExerciseType.PLYOMETRIC: - if (reps) setData.reps = parseInt(reps); - break; - } - - try { - const response = await api.post('/sessions/active/log-set', setData); - if (response.success) { - const { newSet, activeExerciseId } = response; - onSetAdded(newSet); - - if (activePlan && activeExerciseId) { - const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId); - if (nextStepIndex !== -1) { - setCurrentStepIndex(nextStepIndex); - } - } else if (activePlan && !activeExerciseId) { - // Plan is finished - setCurrentStepIndex(activePlan.steps.length); - } - } - } catch (error) { - console.error("Failed to log set:", error); } + } catch (error) { + console.error("Failed to log set:", error); } }; const handleLogSporadicSet = async () => { if (!selectedExercise) return; - // For unilateral exercises, create two sets (LEFT and RIGHT) + const setData: any = { + exerciseId: selectedExercise.id, + }; + if (selectedExercise.isUnilateral) { - const setsToCreate: any[] = []; + setData.side = unilateralSide; + } - if (sameValuesBothSides) { - // Create two identical sets with LEFT and RIGHT sides - const set: any = { - exerciseId: selectedExercise.id, - timestamp: Date.now(), - }; + switch (selectedExercise.type) { + case ExerciseType.STRENGTH: + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); + break; + case ExerciseType.BODYWEIGHT: + if (weight) setData.weight = parseFloat(weight); + if (reps) setData.reps = parseInt(reps); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + break; + case ExerciseType.CARDIO: + if (duration) setData.durationSeconds = parseInt(duration); + if (distance) setData.distanceMeters = parseFloat(distance); + break; + case ExerciseType.STATIC: + if (duration) setData.durationSeconds = parseInt(duration); + setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100; + break; + case ExerciseType.HIGH_JUMP: + if (height) setData.height = parseFloat(height); + break; + case ExerciseType.LONG_JUMP: + if (distance) setData.distanceMeters = parseFloat(distance); + break; + case ExerciseType.PLYOMETRIC: + if (reps) setData.reps = parseInt(reps); + break; + } - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weight) set.weight = parseFloat(weight); - if (reps) set.reps = parseInt(reps); - break; - case ExerciseType.BODYWEIGHT: - if (weight) set.weight = parseFloat(weight); - if (reps) set.reps = parseInt(reps); - set.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (duration) set.durationSeconds = parseInt(duration); - if (distance) set.distanceMeters = parseFloat(distance); - break; - case ExerciseType.STATIC: - if (duration) set.durationSeconds = parseInt(duration); - set.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (height) set.height = parseFloat(height); - break; - case ExerciseType.LONG_JUMP: - if (distance) set.distanceMeters = parseFloat(distance); - break; - case ExerciseType.PLYOMETRIC: - if (reps) set.reps = parseInt(reps); - break; - } - - setsToCreate.push({ ...set, side: 'LEFT' }); - setsToCreate.push({ ...set, side: 'RIGHT' }); - } else { - // Create separate sets for LEFT and RIGHT with different values - const leftSet: any = { - exerciseId: selectedExercise.id, - timestamp: Date.now(), - }; - const rightSet: any = { - exerciseId: selectedExercise.id, - timestamp: Date.now(), - }; - - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weightLeft) leftSet.weight = parseFloat(weightLeft); - if (repsLeft) leftSet.reps = parseInt(repsLeft); - if (weightRight) rightSet.weight = parseFloat(weightRight); - if (repsRight) rightSet.reps = parseInt(repsRight); - break; - case ExerciseType.BODYWEIGHT: - if (weightLeft) leftSet.weight = parseFloat(weightLeft); - if (repsLeft) leftSet.reps = parseInt(repsLeft); - leftSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - if (weightRight) rightSet.weight = parseFloat(weightRight); - if (repsRight) rightSet.reps = parseInt(repsRight); - rightSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (durationLeft) leftSet.durationSeconds = parseInt(durationLeft); - if (distanceLeft) leftSet.distanceMeters = parseFloat(distanceLeft); - if (durationRight) rightSet.durationSeconds = parseInt(durationRight); - if (distanceRight) rightSet.distanceMeters = parseFloat(distanceRight); - break; - case ExerciseType.STATIC: - if (durationLeft) leftSet.durationSeconds = parseInt(durationLeft); - leftSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - if (durationRight) rightSet.durationSeconds = parseInt(durationRight); - rightSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (heightLeft) leftSet.height = parseFloat(heightLeft); - if (heightRight) rightSet.height = parseFloat(heightRight); - break; - case ExerciseType.LONG_JUMP: - if (distanceLeft) leftSet.distanceMeters = parseFloat(distanceLeft); - if (distanceRight) rightSet.distanceMeters = parseFloat(distanceRight); - break; - case ExerciseType.PLYOMETRIC: - if (repsLeft) leftSet.reps = parseInt(repsLeft); - if (repsRight) rightSet.reps = parseInt(repsRight); - break; - } - - setsToCreate.push({ ...leftSet, side: 'LEFT' }); - setsToCreate.push({ ...rightSet, side: 'RIGHT' }); - } - - // Log both sets - try { - for (const set of setsToCreate) { - await logSporadicSet(set); - } + try { + const response = await api.post('/sessions/quick-log/set', setData); + if (response.success) { setSporadicSuccess(true); setTimeout(() => setSporadicSuccess(false), 2000); + + // Refresh quick log session + const sessionRes = await api.get('/sessions/quick-log'); + if (sessionRes.success && sessionRes.session) { + setQuickLogSession(sessionRes.session); + } + // Reset form setWeight(''); setReps(''); setDuration(''); setDistance(''); setHeight(''); - setWeightLeft(''); - setWeightRight(''); - setRepsLeft(''); - setRepsRight(''); - setDurationLeft(''); - setDurationRight(''); - setDistanceLeft(''); - setDistanceRight(''); - setHeightLeft(''); - setHeightRight(''); if (onSporadicSetAdded) onSporadicSetAdded(); - } catch (error) { - console.error("Failed to log unilateral sporadic sets:", error); - } - } else { - // Regular bilateral exercise - single set - const set: any = { - exerciseId: selectedExercise.id, - timestamp: Date.now(), - }; - - switch (selectedExercise.type) { - case ExerciseType.STRENGTH: - if (weight) set.weight = parseFloat(weight); - if (reps) set.reps = parseInt(reps); - break; - case ExerciseType.BODYWEIGHT: - if (weight) set.weight = parseFloat(weight); - if (reps) set.reps = parseInt(reps); - set.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.CARDIO: - if (duration) set.durationSeconds = parseInt(duration); - if (distance) set.distanceMeters = parseFloat(distance); - break; - case ExerciseType.STATIC: - if (duration) set.durationSeconds = parseInt(duration); - set.bodyWeightPercentage = parseFloat(bwPercentage) || 100; - break; - case ExerciseType.HIGH_JUMP: - if (height) set.height = parseFloat(height); - break; - case ExerciseType.LONG_JUMP: - if (distance) set.distanceMeters = parseFloat(distance); - break; - case ExerciseType.PLYOMETRIC: - if (reps) set.reps = parseInt(reps); - break; - } - - try { - const result = await logSporadicSet(set); - if (result) { - setSporadicSuccess(true); - setTimeout(() => setSporadicSuccess(false), 2000); - // Reset form - setWeight(''); - setReps(''); - setDuration(''); - setDistance(''); - setHeight(''); - if (onSporadicSetAdded) onSporadicSetAdded(); - } - } catch (error) { - console.error("Failed to log sporadic set:", error); } + } catch (error) { + console.error("Failed to log quick log set:", error); } }; @@ -721,29 +482,9 @@ export const useTracker = ({ handleCancelEdit, jumpToStep, resetForm, - // Unilateral exercise state - sameValuesBothSides, - setSameValuesBothSides, - weightLeft, - setWeightLeft, - weightRight, - setWeightRight, - repsLeft, - setRepsLeft, - repsRight, - setRepsRight, - durationLeft, - setDurationLeft, - durationRight, - setDurationRight, - distanceLeft, - setDistanceLeft, - distanceRight, - setDistanceRight, - heightLeft, - setHeightLeft, - heightRight, - setHeightRight, - handleToggleSameValues, + unilateralSide, + setUnilateralSide, + quickLogSession, // Export this + loadQuickLogSession, // Export reload function }; }; diff --git a/server/package-lock.json b/server/package-lock.json index 1cafaf5..0eaa564 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@google/generative-ai": "^0.24.1", "@prisma/adapter-better-sqlite3": "^7.1.0", - "@prisma/client": "*", + "@prisma/client": "^6.19.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "*", "better-sqlite3": "^12.5.0", diff --git a/server/package.json b/server/package.json index f92fe2b..2d7f69c 100644 --- a/server/package.json +++ b/server/package.json @@ -11,7 +11,7 @@ "dependencies": { "@google/generative-ai": "^0.24.1", "@prisma/adapter-better-sqlite3": "^7.1.0", - "@prisma/client": "*", + "@prisma/client": "^6.19.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "*", "better-sqlite3": "^12.5.0", diff --git a/server/prisma/dev.db b/server/prisma/dev.db index f05694c..707aff1 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/prisma/migrations/20251204191049_unify_sets/migration.sql b/server/prisma/migrations/20251204191049_unify_sets/migration.sql new file mode 100644 index 0000000..ef068b0 --- /dev/null +++ b/server/prisma/migrations/20251204191049_unify_sets/migration.sql @@ -0,0 +1,54 @@ +/* + Warnings: + + - You are about to drop the `SporadicSet` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropIndex +DROP INDEX "SporadicSet_userId_timestamp_idx"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "SporadicSet"; +PRAGMA foreign_keys=on; + +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_WorkoutSession" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "startTime" DATETIME NOT NULL, + "endTime" DATETIME, + "userBodyWeight" REAL, + "note" TEXT, + "planId" TEXT, + "planName" TEXT, + "type" TEXT NOT NULL DEFAULT 'STANDARD', + CONSTRAINT "WorkoutSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_WorkoutSession" ("endTime", "id", "note", "planId", "planName", "startTime", "userBodyWeight", "userId") SELECT "endTime", "id", "note", "planId", "planName", "startTime", "userBodyWeight", "userId" FROM "WorkoutSession"; +DROP TABLE "WorkoutSession"; +ALTER TABLE "new_WorkoutSession" RENAME TO "WorkoutSession"; +CREATE TABLE "new_WorkoutSet" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionId" TEXT NOT NULL, + "exerciseId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "weight" REAL, + "reps" INTEGER, + "distanceMeters" REAL, + "durationSeconds" INTEGER, + "height" REAL, + "bodyWeightPercentage" REAL, + "completed" BOOLEAN NOT NULL DEFAULT true, + "side" TEXT, + "timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "WorkoutSet_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "WorkoutSession" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "WorkoutSet_exerciseId_fkey" FOREIGN KEY ("exerciseId") REFERENCES "Exercise" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_WorkoutSet" ("completed", "distanceMeters", "durationSeconds", "exerciseId", "id", "order", "reps", "sessionId", "side", "weight") SELECT "completed", "distanceMeters", "durationSeconds", "exerciseId", "id", "order", "reps", "sessionId", "side", "weight" FROM "WorkoutSet"; +DROP TABLE "WorkoutSet"; +ALTER TABLE "new_WorkoutSet" RENAME TO "WorkoutSet"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 833224c..6f0c974 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -25,7 +25,6 @@ model User { exercises Exercise[] plans WorkoutPlan[] weightRecords BodyWeightRecord[] - sporadicSets SporadicSet[] } model BodyWeightRecord { @@ -61,7 +60,6 @@ model Exercise { isUnilateral Boolean @default(false) sets WorkoutSet[] - sporadicSets SporadicSet[] } model WorkoutSession { @@ -74,6 +72,7 @@ model WorkoutSession { note String? planId String? planName String? + type String @default("STANDARD") // STANDARD, QUICK_LOG sets WorkoutSet[] } @@ -90,8 +89,11 @@ model WorkoutSet { reps Int? distanceMeters Float? durationSeconds Int? + height Float? + bodyWeightPercentage Float? completed Boolean @default(true) side String? // LEFT, RIGHT, or null for bilateral + timestamp DateTime @default(now()) } model WorkoutPlan { @@ -105,23 +107,3 @@ model WorkoutPlan { updatedAt DateTime @updatedAt } -model SporadicSet { - id String @id @default(uuid()) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - exerciseId String - exercise Exercise @relation(fields: [exerciseId], references: [id]) - - weight Float? - reps Int? - distanceMeters Float? - durationSeconds Int? - height Float? - bodyWeightPercentage Float? - side String? // LEFT, RIGHT, or null for bilateral - - timestamp DateTime @default(now()) - note String? - - @@index([userId, timestamp]) -} diff --git a/server/sporadic_backup.json b/server/sporadic_backup.json new file mode 100644 index 0000000..7bf3e61 --- /dev/null +++ b/server/sporadic_backup.json @@ -0,0 +1,128 @@ +[ + { + "id": "afc0252b-81c8-4534-b10c-fd328ead82c8", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:25:04.297Z", + "note": null + }, + { + "id": "e772067e-bbea-4e70-83bf-128e6a2feab4", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "RIGHT", + "timestamp": "2025-12-03T21:25:04.335Z", + "note": null + }, + { + "id": "b3b86064-935d-45ee-aab2-b7cf7e1de883", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 4, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:34:13.194Z", + "note": null + }, + { + "id": "688c19fa-2cb2-48b0-a96c-71e894047340", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 4, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "RIGHT", + "timestamp": "2025-12-03T21:34:13.226Z", + "note": null + }, + { + "id": "93db2e6c-5cab-41a1-b3b4-a66e00ebca1c", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "RIGHT", + "timestamp": "2025-12-03T21:44:15.119Z", + "note": null + }, + { + "id": "7e59647f-a115-47ec-9327-5d46df0e56e8", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:44:24.122Z", + "note": null + }, + { + "id": "4dd11f30-f96b-4f9f-b6fd-1968315e06ec", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:53:54.535Z", + "note": null + }, + { + "id": "308c4ec7-7518-45b7-a066-5db1c7e2229e", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:54:31.820Z", + "note": null + }, + { + "id": "c03a8123-05e9-45c0-aac8-587dd6342c27", + "userId": "f9c47b8f-2a34-4157-b5bb-e4a803250a7b", + "exerciseId": "19b3c365-4b2b-448b-8c25-90562aca9a4b", + "weight": 12, + "reps": 13, + "distanceMeters": null, + "durationSeconds": null, + "height": null, + "bodyWeightPercentage": null, + "side": "LEFT", + "timestamp": "2025-12-03T21:58:44.945Z", + "note": null + } +] \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index af6060e..e02c2a7 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -6,7 +6,7 @@ import sessionRoutes from './routes/sessions'; import planRoutes from './routes/plans'; import aiRoutes from './routes/ai'; import weightRoutes from './routes/weight'; -import sporadicSetsRoutes from './routes/sporadic-sets'; + import bcrypt from 'bcryptjs'; import { PrismaClient } from '@prisma/client'; import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'; @@ -63,7 +63,7 @@ app.use('/api/sessions', sessionRoutes); app.use('/api/plans', planRoutes); app.use('/api/ai', aiRoutes); app.use('/api/weight', weightRoutes); -app.use('/api/sporadic-sets', sporadicSetsRoutes); + app.get('/', (req, res) => { res.send('GymFlow AI API is running'); diff --git a/server/src/routes/sessions.ts b/server/src/routes/sessions.ts index 200996d..e776523 100644 --- a/server/src/routes/sessions.ts +++ b/server/src/routes/sessions.ts @@ -29,7 +29,18 @@ router.get('/', async (req: any, res) => { include: { sets: { include: { exercise: true } } }, orderBy: { startTime: 'desc' } }); - res.json(sessions); + + // Map exerciseName and type onto each set for frontend convenience + const mappedSessions = sessions.map(session => ({ + ...session, + sets: session.sets.map(set => ({ + ...set, + exerciseName: set.exercise.name, + type: set.exercise.type + })) + })); + + res.json(mappedSessions); } catch (error) { res.status(500).json({ error: 'Server error' }); } @@ -83,7 +94,11 @@ router.post('/', async (req: any, res) => { // If creating a new active session (endTime is null), check if one already exists if (!end) { const active = await prisma.workoutSession.findFirst({ - where: { userId, endTime: null } + where: { + userId, + endTime: null, + type: 'STANDARD' // Only check for standard sessions, not Quick Log + } }); if (active) { return res.status(400).json({ error: 'An active session already exists' }); @@ -149,7 +164,8 @@ router.get('/active', async (req: any, res) => { const activeSession = await prisma.workoutSession.findFirst({ where: { userId, - endTime: null + endTime: null, + type: 'STANDARD' }, include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } } }); @@ -228,15 +244,119 @@ router.put('/active', async (req: any, res) => { } }); +// Get today's quick log session +router.get('/quick-log', async (req: any, res) => { + try { + const userId = req.user.userId; + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 59, 999); + + const session = await prisma.workoutSession.findFirst({ + where: { + userId, + type: 'QUICK_LOG', + startTime: { + gte: startOfDay, + lte: endOfDay + } + }, + include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } } + }); + + if (!session) { + return res.json({ success: true, session: null }); + } + + // Map exerciseName and type onto sets + const mappedSession = { + ...session, + sets: session.sets.map(set => ({ + ...set, + exerciseName: set.exercise.name, + type: set.exercise.type + })) + }; + + res.json({ success: true, session: mappedSession }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Log a set to today's quick log session +router.post('/quick-log/set', async (req: any, res) => { + try { + const userId = req.user.userId; + const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; + + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 59, 999); + + // Find or create today's quick log session + let session = await prisma.workoutSession.findFirst({ + where: { + userId, + type: 'QUICK_LOG', + startTime: { + gte: startOfDay, + lte: endOfDay + } + } + }); + + if (!session) { + session = await prisma.workoutSession.create({ + data: { + userId, + startTime: startOfDay, + type: 'QUICK_LOG', + note: 'Daily Quick Log' + } + }); + } + + // Create the set + const newSet = await prisma.workoutSet.create({ + data: { + sessionId: session.id, + exerciseId, + order: 0, + weight: weight ? parseFloat(weight) : null, + reps: reps ? parseInt(reps) : null, + distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, + durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, + side: side || null + }, + include: { exercise: true } + }); + + const mappedSet = { + ...newSet, + exerciseName: newSet.exercise.name, + type: newSet.exercise.type + }; + + res.json({ success: true, set: mappedSet }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + // Log a set to the active session router.post('/active/log-set', async (req: any, res) => { try { const userId = req.user.userId; - const { exerciseId, reps, weight, distanceMeters, durationSeconds } = req.body; + const { exerciseId, reps, weight, distanceMeters, durationSeconds, side } = req.body; // Find active session const activeSession = await prisma.workoutSession.findFirst({ - where: { userId, endTime: null }, + where: { userId, endTime: null, type: 'STANDARD' }, include: { sets: true } }); @@ -257,6 +377,7 @@ router.post('/active/log-set', async (req: any, res) => { weight: weight ? parseFloat(weight) : null, distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, + side: side || null, completed: true }, include: { exercise: true } @@ -324,7 +445,7 @@ router.put('/active/set/:setId', async (req: any, res) => { const { setId } = req.params; const { reps, weight, distanceMeters, durationSeconds } = req.body; - // Find active session + // Find active session (STANDARD or QUICK_LOG) const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, }); @@ -358,13 +479,58 @@ router.put('/active/set/:setId', async (req: any, res) => { } }); +// Update a set in the active session (STANDARD or QUICK_LOG) +router.patch('/active/set/:setId', async (req: any, res) => { + try { + const userId = req.user.userId; + const { setId } = req.params; + const { reps, weight, distanceMeters, durationSeconds, height, bodyWeightPercentage, side, note } = req.body; + + // Find active session (STANDARD or QUICK_LOG) + const activeSession = await prisma.workoutSession.findFirst({ + where: { userId, endTime: null }, + }); + + if (!activeSession) { + return res.status(404).json({ error: 'No active session found' }); + } + + const updatedSet = await prisma.workoutSet.update({ + where: { id: setId }, + data: { + reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined, + weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined, + distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined, + durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined, + height: height !== undefined ? (height ? parseFloat(height) : null) : undefined, + bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined, + side: side !== undefined ? side : undefined, + note: note !== undefined ? note : undefined, + }, + include: { exercise: true } + }); + + const mappedUpdatedSet = { + ...updatedSet, + exerciseName: updatedSet.exercise.name, + type: updatedSet.exercise.type + }; + + res.json({ success: true, updatedSet: mappedUpdatedSet }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + // Delete a set from the active session router.delete('/active/set/:setId', async (req: any, res) => { try { const userId = req.user.userId; const { setId } = req.params; - // Find active session + // Find active session (STANDARD or QUICK_LOG) const activeSession = await prisma.workoutSession.findFirst({ where: { userId, endTime: null }, }); @@ -394,7 +560,8 @@ router.delete('/active', async (req: any, res) => { await prisma.workoutSession.deleteMany({ where: { userId, - endTime: null + endTime: null, + type: 'STANDARD' } }); @@ -419,4 +586,113 @@ router.delete('/:id', async (req: any, res) => { } }); +// Get today's quick log session +router.get('/quick-log', async (req: any, res) => { + try { + const userId = req.user.userId; + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 59, 999); + + const session = await prisma.workoutSession.findFirst({ + where: { + userId, + type: 'QUICK_LOG', + startTime: { + gte: startOfDay, + lte: endOfDay + } + }, + include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } } + }); + + if (!session) { + return res.json({ success: true, session: null }); + } + + // Map exercise properties to sets for frontend compatibility + const mappedSession = { + ...session, + sets: session.sets.map((set: any) => ({ + ...set, + exerciseName: set.exercise.name, + type: set.exercise.type + })) + }; + + res.json({ success: true, session: mappedSession }); + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + +// Log a set to today's quick log session +router.post('/quick-log/set', async (req: any, res) => { + try { + const userId = req.user.userId; + const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; + + const startOfDay = new Date(); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 59, 999); + + // Find or create today's quick log session + let session = await prisma.workoutSession.findFirst({ + where: { + userId, + type: 'QUICK_LOG', + startTime: { + gte: startOfDay, + lte: endOfDay + } + } + }); + + if (!session) { + session = await prisma.workoutSession.create({ + data: { + userId, + startTime: startOfDay, + type: 'QUICK_LOG', + note: 'Daily Quick Log' + } + }); + } + + // Create the set + const newSet = await prisma.workoutSet.create({ + data: { + sessionId: session.id, + exerciseId, + order: 0, // Order not strictly enforced for quick log + weight: weight ? parseFloat(weight) : null, + reps: reps ? parseInt(reps) : null, + distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, + durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, + height: height ? parseFloat(height) : null, + bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null, + side: side || null, + completed: true, + timestamp: new Date() + }, + include: { exercise: true } + }); + + const mappedSet = { + ...newSet, + exerciseName: newSet.exercise.name, + type: newSet.exercise.type + }; + + res.json({ success: true, newSet: mappedSet }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: 'Server error' }); + } +}); + export default router; diff --git a/server/src/routes/sporadic-sets.ts b/server/src/routes/sporadic-sets.ts deleted file mode 100644 index 1425921..0000000 --- a/server/src/routes/sporadic-sets.ts +++ /dev/null @@ -1,195 +0,0 @@ -import express from 'express'; -import jwt from 'jsonwebtoken'; -import prisma from '../lib/prisma'; - -const router = express.Router(); -const JWT_SECRET = process.env.JWT_SECRET || 'secret'; - -const authenticate = (req: any, res: any, next: any) => { - const token = req.headers.authorization?.split(' ')[1]; - if (!token) return res.status(401).json({ error: 'Unauthorized' }); - - try { - const decoded = jwt.verify(token, JWT_SECRET) as any; - req.user = decoded; - next(); - } catch { - res.status(401).json({ error: 'Invalid token' }); - } -}; - -router.use(authenticate); - -// Get all sporadic sets for the authenticated user -router.get('/', async (req: any, res) => { - try { - const userId = req.user.userId; - const sporadicSets = await prisma.sporadicSet.findMany({ - where: { userId }, - include: { exercise: true }, - orderBy: { timestamp: 'desc' } - }); - - // Map to include exercise name and type - const mappedSets = sporadicSets.map(set => ({ - id: set.id, - exerciseId: set.exerciseId, - exerciseName: set.exercise.name, - type: set.exercise.type, - weight: set.weight, - reps: set.reps, - distanceMeters: set.distanceMeters, - durationSeconds: set.durationSeconds, - height: set.height, - bodyWeightPercentage: set.bodyWeightPercentage, - timestamp: set.timestamp.getTime(), - note: set.note - })); - - res.json({ success: true, sporadicSets: mappedSets }); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Create a new sporadic set -router.post('/', async (req: any, res) => { - try { - const userId = req.user.userId; - const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; - - if (!exerciseId) { - return res.status(400).json({ error: 'Exercise ID is required' }); - } - - // Verify that the exercise exists - const exercise = await prisma.exercise.findUnique({ - where: { id: exerciseId } - }); - - if (!exercise) { - return res.status(400).json({ error: `Exercise with ID ${exerciseId} not found` }); - } - - - const sporadicSet = await prisma.sporadicSet.create({ - data: { - userId, - exerciseId, - weight: weight ? parseFloat(weight) : null, - reps: reps ? parseInt(reps) : null, - distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, - durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, - height: height ? parseFloat(height) : null, - bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null, - note: note || null, - side: side || null - }, - include: { exercise: true } - }); - - const mappedSet = { - id: sporadicSet.id, - exerciseId: sporadicSet.exerciseId, - exerciseName: sporadicSet.exercise.name, - type: sporadicSet.exercise.type, - weight: sporadicSet.weight, - reps: sporadicSet.reps, - distanceMeters: sporadicSet.distanceMeters, - durationSeconds: sporadicSet.durationSeconds, - height: sporadicSet.height, - bodyWeightPercentage: sporadicSet.bodyWeightPercentage, - timestamp: sporadicSet.timestamp.getTime(), - note: sporadicSet.note, - side: sporadicSet.side - }; - - res.json({ success: true, sporadicSet: mappedSet }); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Update a sporadic set -router.put('/:id', async (req: any, res) => { - try { - const userId = req.user.userId; - const { id } = req.params; - const { weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; - - // Verify ownership - const existing = await prisma.sporadicSet.findFirst({ - where: { id, userId } - }); - - if (!existing) { - return res.status(404).json({ error: 'Sporadic set not found' }); - } - - const updated = await prisma.sporadicSet.update({ - where: { id }, - data: { - weight: weight !== undefined ? (weight ? parseFloat(weight) : null) : undefined, - reps: reps !== undefined ? (reps ? parseInt(reps) : null) : undefined, - distanceMeters: distanceMeters !== undefined ? (distanceMeters ? parseFloat(distanceMeters) : null) : undefined, - durationSeconds: durationSeconds !== undefined ? (durationSeconds ? parseInt(durationSeconds) : null) : undefined, - height: height !== undefined ? (height ? parseFloat(height) : null) : undefined, - bodyWeightPercentage: bodyWeightPercentage !== undefined ? (bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null) : undefined, - note: note !== undefined ? note : undefined, - side: side !== undefined ? side : undefined - }, - include: { exercise: true } - }); - - const mappedSet = { - id: updated.id, - exerciseId: updated.exerciseId, - exerciseName: updated.exercise.name, - type: updated.exercise.type, - weight: updated.weight, - reps: updated.reps, - distanceMeters: updated.distanceMeters, - durationSeconds: updated.durationSeconds, - height: updated.height, - bodyWeightPercentage: updated.bodyWeightPercentage, - timestamp: updated.timestamp.getTime(), - note: updated.note, - side: updated.side - }; - - res.json({ success: true, sporadicSet: mappedSet }); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Delete a sporadic set -router.delete('/:id', async (req: any, res) => { - try { - const userId = req.user.userId; - const { id } = req.params; - - // Verify ownership - const existing = await prisma.sporadicSet.findFirst({ - where: { id, userId } - }); - - if (!existing) { - return res.status(404).json({ error: 'Sporadic set not found' }); - } - - await prisma.sporadicSet.delete({ - where: { id } - }); - - res.json({ success: true }); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - -export default router; diff --git a/server/src/scripts/backupSporadicSets.ts b/server/src/scripts/backupSporadicSets.ts new file mode 100644 index 0000000..f7d4d75 --- /dev/null +++ b/server/src/scripts/backupSporadicSets.ts @@ -0,0 +1,21 @@ +import { PrismaClient } from '@prisma/client'; +import fs from 'fs'; +import path from 'path'; + +const prisma = new PrismaClient(); + +async function backup() { + try { + console.log('Starting backup...'); + const sporadicSets = await prisma.sporadicSet.findMany(); + const backupPath = path.join(__dirname, '../../sporadic_backup.json'); + fs.writeFileSync(backupPath, JSON.stringify(sporadicSets, null, 2)); + console.log(`Backed up ${sporadicSets.length} sporadic sets to ${backupPath}`); + } catch (error) { + console.error('Backup failed:', error); + } finally { + await prisma.$disconnect(); + } +} + +backup(); diff --git a/server/src/scripts/restoreSporadicSets.ts b/server/src/scripts/restoreSporadicSets.ts new file mode 100644 index 0000000..c85726d --- /dev/null +++ b/server/src/scripts/restoreSporadicSets.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client'; +import fs from 'fs'; +import path from 'path'; + +const prisma = new PrismaClient(); + +async function restore() { + try { + const backupPath = path.join(__dirname, '../../sporadic_backup.json'); + if (!fs.existsSync(backupPath)) { + console.error('Backup file not found!'); + return; + } + + const sporadicSets = JSON.parse(fs.readFileSync(backupPath, 'utf-8')); + console.log(`Found ${sporadicSets.length} sporadic sets to restore.`); + + for (const set of sporadicSets) { + const date = new Date(set.timestamp); + const startOfDay = new Date(date); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(date); + endOfDay.setHours(23, 59, 59, 999); + + // Find or create a QUICK_LOG session for this day + let session = await prisma.workoutSession.findFirst({ + where: { + userId: set.userId, + type: 'QUICK_LOG', + startTime: { + gte: startOfDay, + lte: endOfDay + } + } + }); + + if (!session) { + session = await prisma.workoutSession.create({ + data: { + userId: set.userId, + startTime: startOfDay, // Use start of day as session start + type: 'QUICK_LOG', + note: 'Daily Quick Log' + } + }); + console.log(`Created new QUICK_LOG session for ${startOfDay.toISOString()}`); + } + + // Create the WorkoutSet + await prisma.workoutSet.create({ + data: { + sessionId: session.id, + exerciseId: set.exerciseId, + order: 0, // Order doesn't matter much for sporadic sets, or we could increment + weight: set.weight, + reps: set.reps, + distanceMeters: set.distanceMeters, + durationSeconds: set.durationSeconds, + height: set.height, + bodyWeightPercentage: set.bodyWeightPercentage, + side: set.side, + timestamp: new Date(set.timestamp), + completed: true + } + }); + } + + console.log('Restoration complete!'); + + } catch (error) { + console.error('Restoration failed:', error); + } finally { + await prisma.$disconnect(); + } +} + +restore(); diff --git a/types.ts b/types.ts index 783a2df..a558e3d 100644 --- a/types.ts +++ b/types.ts @@ -22,6 +22,7 @@ export interface WorkoutSet { bodyWeightPercentage?: number; // Percentage of bodyweight used (e.g. 65 for pushups) timestamp: number; side?: 'LEFT' | 'RIGHT'; // For unilateral exercises + completed: boolean; } export interface WorkoutSession { @@ -33,6 +34,7 @@ export interface WorkoutSession { userBodyWeight?: number; planId?: string; // Link to a plan if used planName?: string; + type: 'STANDARD' | 'QUICK_LOG'; } export interface ExerciseDef { @@ -81,22 +83,6 @@ export interface BodyWeightRecord { dateStr: string; // YYYY-MM-DD } -export interface SporadicSet { - id: string; - exerciseId: string; - exerciseName: string; - type: ExerciseType; - reps?: number; - weight?: number; - durationSeconds?: number; - distanceMeters?: number; - height?: number; - bodyWeightPercentage?: number; - timestamp: number; - note?: string; - side?: 'LEFT' | 'RIGHT'; // For unilateral exercises -} - export interface User { id: string; email: string;