Ongoing workout session data is persistent now
This commit is contained in:
80
App.tsx
80
App.tsx
@@ -9,7 +9,7 @@ import Plans from './components/Plans';
|
||||
import Login from './components/Login';
|
||||
import Profile from './components/Profile';
|
||||
import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types';
|
||||
import { getSessions, saveSession, deleteSession, getPlans } from './services/storage';
|
||||
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession } from './services/storage';
|
||||
import { getCurrentUserProfile, getMe } from './services/auth';
|
||||
import { getSystemLanguage } from './services/i18n';
|
||||
import { generateId } from './utils/uuid';
|
||||
@@ -35,6 +35,20 @@ function App() {
|
||||
const res = await getMe();
|
||||
if (res.success && res.user) {
|
||||
setCurrentUser(res.user);
|
||||
|
||||
// Restore active workout session from database
|
||||
const activeSession = await getActiveSession(res.user.id);
|
||||
if (activeSession) {
|
||||
setActiveSession(activeSession);
|
||||
// Restore plan if session has planId
|
||||
if (activeSession.planId) {
|
||||
const plans = await getPlans(res.user.id);
|
||||
const plan = plans.find(p => p.id === activeSession.planId);
|
||||
if (plan) {
|
||||
setActivePlan(plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
@@ -78,7 +92,7 @@ function App() {
|
||||
setCurrentUser(updatedUser);
|
||||
};
|
||||
|
||||
const handleStartSession = (plan?: WorkoutPlan, startWeight?: number) => {
|
||||
const handleStartSession = async (plan?: WorkoutPlan, startWeight?: number) => {
|
||||
if (!currentUser) return;
|
||||
|
||||
// Get latest weight from profile or default
|
||||
@@ -97,12 +111,15 @@ function App() {
|
||||
setActivePlan(plan || null);
|
||||
setActiveSession(newSession);
|
||||
setCurrentTab('TRACK');
|
||||
|
||||
// Save to database immediately
|
||||
await saveSession(currentUser.id, newSession);
|
||||
};
|
||||
|
||||
const handleEndSession = async () => {
|
||||
if (activeSession && currentUser) {
|
||||
const finishedSession = { ...activeSession, endTime: Date.now() };
|
||||
await saveSession(currentUser.id, finishedSession);
|
||||
await updateActiveSession(currentUser.id, finishedSession);
|
||||
setSessions(prev => [finishedSession, ...prev]);
|
||||
setActiveSession(null);
|
||||
setActivePlan(null);
|
||||
@@ -115,39 +132,47 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSet = (set: WorkoutSet) => {
|
||||
if (activeSession) {
|
||||
setActiveSession(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
sets: [...prev.sets, set]
|
||||
const handleAddSet = async (set: WorkoutSet) => {
|
||||
if (activeSession && currentUser) {
|
||||
const updatedSession = {
|
||||
...activeSession,
|
||||
sets: [...activeSession.sets, set]
|
||||
};
|
||||
});
|
||||
setActiveSession(updatedSession);
|
||||
// Save to database
|
||||
await updateActiveSession(currentUser.id, updatedSession);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSetFromActive = (setId: string) => {
|
||||
if (activeSession) {
|
||||
setActiveSession(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
sets: prev.sets.filter(s => s.id !== setId)
|
||||
const handleRemoveSetFromActive = async (setId: string) => {
|
||||
if (activeSession && currentUser) {
|
||||
const updatedSession = {
|
||||
...activeSession,
|
||||
sets: activeSession.sets.filter(s => s.id !== setId)
|
||||
};
|
||||
});
|
||||
setActiveSession(updatedSession);
|
||||
// Save to database
|
||||
await updateActiveSession(currentUser.id, updatedSession);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSetInActive = (updatedSet: WorkoutSet) => {
|
||||
if (activeSession) {
|
||||
setActiveSession(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
sets: prev.sets.map(s => s.id === updatedSet.id ? updatedSet : s)
|
||||
const handleUpdateSetInActive = async (updatedSet: WorkoutSet) => {
|
||||
if (activeSession && currentUser) {
|
||||
const updatedSession = {
|
||||
...activeSession,
|
||||
sets: activeSession.sets.map(s => s.id === updatedSet.id ? updatedSet : s)
|
||||
};
|
||||
});
|
||||
setActiveSession(updatedSession);
|
||||
// Save to database
|
||||
await updateActiveSession(currentUser.id, updatedSession);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuitSession = async () => {
|
||||
if (currentUser) {
|
||||
await deleteActiveSession(currentUser.id);
|
||||
setActiveSession(null);
|
||||
setActivePlan(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,6 +209,7 @@ function App() {
|
||||
activePlan={activePlan}
|
||||
onSessionStart={handleStartSession}
|
||||
onSessionEnd={handleEndSession}
|
||||
onSessionQuit={handleQuitSession}
|
||||
onSetAdded={handleAddSet}
|
||||
onRemoveSet={handleRemoveSetFromActive}
|
||||
onUpdateSet={handleUpdateSetInActive}
|
||||
|
||||
@@ -75,8 +75,11 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user.id, user.role, JSON.stringify(user.profile)]);
|
||||
|
||||
const refreshUserList = () => {
|
||||
setAllUsers(getUsers());
|
||||
const refreshUserList = async () => {
|
||||
const res = await getUsers();
|
||||
if (res.success && res.users) {
|
||||
setAllUsers(res.users);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshExercises = async () => {
|
||||
@@ -143,16 +146,16 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminDeleteUser = (uid: string) => {
|
||||
const handleAdminDeleteUser = async (uid: string) => {
|
||||
if (confirm(t('delete_confirm', lang))) {
|
||||
deleteUser(uid);
|
||||
refreshUserList();
|
||||
await deleteUser(uid);
|
||||
await refreshUserList();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminBlockUser = (uid: string, isBlocked: boolean) => {
|
||||
toggleBlockUser(uid, isBlocked);
|
||||
refreshUserList();
|
||||
const handleAdminBlockUser = async (uid: string, isBlocked: boolean) => {
|
||||
await toggleBlockUser(uid, isBlocked);
|
||||
await refreshUserList();
|
||||
};
|
||||
|
||||
const handleAdminResetPass = (uid: string) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ interface TrackerProps {
|
||||
activePlan: WorkoutPlan | null;
|
||||
onSessionStart: (plan?: WorkoutPlan, startWeight?: number) => void;
|
||||
onSessionEnd: () => void;
|
||||
onSessionQuit: () => void;
|
||||
onSetAdded: (set: WorkoutSet) => void;
|
||||
onRemoveSet: (setId: string) => void;
|
||||
onUpdateSet: (set: WorkoutSet) => void;
|
||||
@@ -23,7 +24,7 @@ interface TrackerProps {
|
||||
import FilledInput from './FilledInput';
|
||||
import ExerciseModal from './ExerciseModal';
|
||||
|
||||
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSetAdded, onRemoveSet, onUpdateSet, lang }) => {
|
||||
const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, activePlan, onSessionStart, onSessionEnd, onSessionQuit, onSetAdded, onRemoveSet, onUpdateSet, lang }) => {
|
||||
const [exercises, setExercises] = useState<ExerciseDef[]>([]);
|
||||
const [plans, setPlans] = useState<WorkoutPlan[]>([]);
|
||||
const [selectedExercise, setSelectedExercise] = useState<ExerciseDef | null>(null);
|
||||
@@ -665,8 +666,7 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowQuitConfirm(false);
|
||||
// Quit without saving - just navigate away or reset
|
||||
window.location.reload();
|
||||
onSessionQuit();
|
||||
}}
|
||||
className="px-6 py-2.5 rounded-full bg-green-600 text-white font-medium hover:bg-green-700"
|
||||
>
|
||||
|
||||
Binary file not shown.
@@ -18,7 +18,7 @@ const app = express();
|
||||
// -------------------------------------------------------------------
|
||||
async function ensureAdminUser() {
|
||||
const adminEmail = process.env.ADMIN_EMAIL || 'admin@gymflow.ai';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || 'admin1234';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
|
||||
@@ -62,6 +62,47 @@ router.post('/login', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Register
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({ where: { email } });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'User already exists' });
|
||||
}
|
||||
|
||||
if (!password || password.length < 4) {
|
||||
return res.status(400).json({ error: 'Password too short' });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role: 'USER',
|
||||
profile: {
|
||||
create: {
|
||||
weight: 70
|
||||
}
|
||||
}
|
||||
},
|
||||
include: { profile: true }
|
||||
});
|
||||
|
||||
const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET);
|
||||
const { password: _, ...userSafe } = user;
|
||||
|
||||
res.json({ success: true, user: userSafe, token });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Change Password
|
||||
router.post('/change-password', async (req, res) => {
|
||||
|
||||
@@ -138,4 +179,92 @@ router.patch('/profile', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Admin: Get All Users
|
||||
router.get('/users', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
if (decoded.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isBlocked: true,
|
||||
isFirstLogin: true,
|
||||
profile: true
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ success: true, users });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin: Delete User
|
||||
router.delete('/users/:id', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
if (decoded.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
// Prevent deleting self
|
||||
if (id === decoded.userId) {
|
||||
return res.status(400).json({ error: 'Cannot delete yourself' });
|
||||
}
|
||||
|
||||
await prisma.user.delete({ where: { id } });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin: Toggle Block User
|
||||
router.patch('/users/:id/block', async (req, res) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
if (!token) return res.status(401).json({ error: 'Unauthorized' });
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||
if (decoded.role !== 'ADMIN') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { block } = req.body;
|
||||
|
||||
// Prevent blocking self
|
||||
if (id === decoded.userId) {
|
||||
return res.status(400).json({ error: 'Cannot block yourself' });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { isBlocked: block }
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -133,6 +133,121 @@ router.post('/', async (req: any, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get active session (session without endTime)
|
||||
router.get('/active', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const activeSession = await prisma.workoutSession.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
endTime: null
|
||||
},
|
||||
include: { sets: { include: { exercise: true }, orderBy: { order: 'asc' } } }
|
||||
});
|
||||
|
||||
if (!activeSession) {
|
||||
return res.json({ success: true, session: null });
|
||||
}
|
||||
|
||||
res.json({ success: true, session: activeSession });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Update active session (for real-time set updates)
|
||||
router.put('/active', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body;
|
||||
|
||||
// Convert timestamps to Date objects if they are numbers
|
||||
const start = new Date(startTime);
|
||||
const end = endTime ? new Date(endTime) : null;
|
||||
const weight = userBodyWeight ? parseFloat(userBodyWeight) : null;
|
||||
|
||||
// Check if session exists and belongs to user
|
||||
const existing = await prisma.workoutSession.findFirst({
|
||||
where: { id, userId }
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
// Delete existing sets to replace them
|
||||
await prisma.workoutSet.deleteMany({ where: { sessionId: id } });
|
||||
|
||||
const updated = await prisma.workoutSession.update({
|
||||
where: { id },
|
||||
data: {
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
userBodyWeight: weight,
|
||||
note,
|
||||
planId,
|
||||
planName,
|
||||
sets: {
|
||||
create: sets.map((s: any, idx: number) => ({
|
||||
exerciseId: s.exerciseId,
|
||||
order: idx,
|
||||
weight: s.weight,
|
||||
reps: s.reps,
|
||||
distanceMeters: s.distanceMeters,
|
||||
durationSeconds: s.durationSeconds,
|
||||
completed: s.completed !== undefined ? s.completed : true
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { sets: { include: { exercise: true } } }
|
||||
});
|
||||
|
||||
// Update user profile weight if session has weight and is finished
|
||||
if (weight && end) {
|
||||
await prisma.userProfile.upsert({
|
||||
where: { userId },
|
||||
create: { userId, weight },
|
||||
update: { weight }
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, session: updated });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete active session (quit without saving)
|
||||
router.delete('/active', async (req: any, res) => {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
|
||||
// Find active session
|
||||
const activeSession = await prisma.workoutSession.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
endTime: null
|
||||
}
|
||||
});
|
||||
|
||||
if (!activeSession) {
|
||||
return res.json({ success: true, message: 'No active session found' });
|
||||
}
|
||||
|
||||
// Delete the session (cascade will delete sets)
|
||||
await prisma.workoutSession.delete({
|
||||
where: { id: activeSession.id }
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session
|
||||
router.delete('/:id', async (req: any, res) => {
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,15 @@ export const api = {
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
put: async (endpoint: string, data: any) => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'PUT',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
return res.json();
|
||||
},
|
||||
delete: async (endpoint: string) => {
|
||||
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||
method: 'DELETE',
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { User, UserRole, UserProfile } from '../types';
|
||||
import { api, setAuthToken, removeAuthToken } from './api';
|
||||
|
||||
export const getUsers = (): any[] => {
|
||||
// Not used in frontend anymore
|
||||
return [];
|
||||
export const getUsers = async (): Promise<{ success: boolean; users?: User[]; error?: string }> => {
|
||||
try {
|
||||
const res = await api.get('/auth/users');
|
||||
return res;
|
||||
} catch (e) {
|
||||
return { success: false, error: 'Failed to fetch users' };
|
||||
}
|
||||
};
|
||||
|
||||
export const login = async (email: string, password: string): Promise<{ success: boolean; user?: User; error?: string }> => {
|
||||
@@ -43,11 +47,21 @@ export const createUser = async (email: string, password: string): Promise<{ suc
|
||||
};
|
||||
|
||||
export const deleteUser = async (userId: string) => {
|
||||
// Admin only, not implemented in frontend UI yet
|
||||
try {
|
||||
const res = await api.delete(`/auth/users/${userId}`);
|
||||
return res;
|
||||
} catch (e) {
|
||||
return { success: false, error: 'Failed to delete user' };
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleBlockUser = (userId: string, block: boolean) => {
|
||||
// Admin only
|
||||
export const toggleBlockUser = async (userId: string, block: boolean) => {
|
||||
try {
|
||||
const res = await api.patch(`/auth/users/${userId}/block`, { block });
|
||||
return res;
|
||||
} catch (e) {
|
||||
return { success: false, error: 'Failed to update user status' };
|
||||
}
|
||||
};
|
||||
|
||||
export const adminResetPassword = (userId: string, newPass: string) => {
|
||||
|
||||
@@ -24,6 +24,37 @@ export const saveSession = async (userId: string, session: WorkoutSession): Prom
|
||||
await api.post('/sessions', session);
|
||||
};
|
||||
|
||||
export const getActiveSession = async (userId: string): Promise<WorkoutSession | null> => {
|
||||
try {
|
||||
const response = await api.get('/sessions/active');
|
||||
if (!response.success || !response.session) {
|
||||
return null;
|
||||
}
|
||||
const session = response.session;
|
||||
// Convert ISO date strings to timestamps
|
||||
return {
|
||||
...session,
|
||||
startTime: new Date(session.startTime).getTime(),
|
||||
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined,
|
||||
sets: session.sets.map((set: any) => ({
|
||||
...set,
|
||||
exerciseName: set.exercise?.name || 'Unknown',
|
||||
type: set.exercise?.type || 'STRENGTH'
|
||||
}))
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateActiveSession = async (userId: string, session: WorkoutSession): Promise<void> => {
|
||||
await api.put('/sessions/active', session);
|
||||
};
|
||||
|
||||
export const deleteActiveSession = async (userId: string): Promise<void> => {
|
||||
await api.delete('/sessions/active');
|
||||
};
|
||||
|
||||
export const deleteSession = async (userId: string, id: string): Promise<void> => {
|
||||
await api.delete(`/sessions/${id}`);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user