1. Session end time saving. 2. Plan Id to the saved session. 3. History page redesigned (attributes moved, sets list hidden. 4. Session duration added. 5. Session start and end time logging fixed.

This commit is contained in:
AG
2025-11-24 23:22:09 +02:00
parent cce1e58c7b
commit 72867668d4
7 changed files with 116 additions and 64 deletions

2
.gitignore vendored
View File

@@ -11,7 +11,7 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
server/prisma/dev.db *.db
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@@ -41,6 +41,23 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
return new Date(value).getTime(); return new Date(value).getTime();
}; };
const formatDuration = (startTime: number, endTime?: number) => {
if (!endTime || isNaN(endTime) || isNaN(startTime)) return '';
const durationMs = endTime - startTime;
if (durationMs < 0 || isNaN(durationMs)) return '';
const hours = Math.floor(durationMs / 3600000);
const minutes = Math.floor((durationMs % 3600000) / 60000);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
if (minutes < 1) {
return '<1m';
}
return `${minutes}m`;
};
const handleSaveEdit = () => { const handleSaveEdit = () => {
if (editingSession && onUpdateSession) { if (editingSession && onUpdateSession) {
onUpdateSession(editingSession); onUpdateSession(editingSession);
@@ -91,81 +108,66 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
const totalWork = calculateSessionWork(session); const totalWork = calculateSessionWork(session);
return ( return (
<div key={session.id} className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20"> <div
<div className="flex justify-between items-start mb-4 border-b border-outline-variant pb-3"> key={session.id}
<div className="flex items-center gap-3"> className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20 cursor-pointer hover:bg-surface-container-high transition-colors"
<div className="w-10 h-10 rounded-full bg-tertiary-container text-on-tertiary-container flex items-center justify-center"> onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))}
<Calendar size={20} /> >
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-3 flex-wrap">
<span className="font-medium text-on-surface text-lg">
{new Date(session.startTime).toISOString().split('T')[0]}
</span>
<span className="text-sm text-on-surface-variant">
{new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
{session.endTime && (
<span className="text-sm text-on-surface-variant">
{formatDuration(session.startTime, session.endTime)}
</span>
)}
<span className="text-sm text-on-surface-variant">
{session.planName || t('no_plan', lang)}
</span>
{session.userBodyWeight && (
<span className="px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface text-xs">
{session.userBodyWeight}kg
</span>
)}
</div> </div>
<div> <div className="mt-2 text-xs text-on-surface-variant">
<div className="font-medium text-on-surface text-lg"> {t('sets_count', lang)}: <span className="text-on-surface font-medium">{session.sets.length}</span>
{new Date(session.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { weekday: 'long', day: 'numeric', month: 'long' })} {totalWork > 0 && (
</div> <span className="ml-4 inline-flex items-center gap-1">
<div className="text-xs text-on-surface-variant flex items-center gap-2"> <Scale size={12} />
<span>{new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> {(totalWork / 1000).toFixed(1)}t
{session.userBodyWeight && <span className="px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface">{session.userBodyWeight}kg</span>} </span>
</div> )}
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))} onClick={(e) => {
e.stopPropagation();
setEditingSession(JSON.parse(JSON.stringify(session)));
}}
className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors" className="p-2 text-on-surface-variant hover:text-primary hover:bg-surface-container-high rounded-full transition-colors"
> >
<Pencil size={20} /> <Pencil size={20} />
</button> </button>
<button <button
onClick={() => setDeletingId(session.id)} onClick={(e) => {
e.stopPropagation();
setDeletingId(session.id);
}}
className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors" className="p-2 text-on-surface-variant hover:text-error hover:bg-surface-container-high rounded-full transition-colors"
> >
<Trash2 size={20} /> <Trash2 size={20} />
</button> </button>
</div> </div>
</div> </div>
<div className="space-y-2">
{Array.from(new Set(session.sets.map(s => s.exerciseName))).slice(0, 4).map(exName => {
const sets = session.sets.filter(s => s.exerciseName === exName);
const count = sets.length;
const bestSet = sets[0];
let detail = "";
if (bestSet.type === ExerciseType.HIGH_JUMP) detail = `${t('max', lang)}: ${Math.max(...sets.map(s => s.height || 0))}cm`;
else if (bestSet.type === ExerciseType.LONG_JUMP) detail = `${t('max', lang)}: ${Math.max(...sets.map(s => s.distanceMeters || 0))}m`;
else if (bestSet.type === ExerciseType.STRENGTH) detail = `${t('upto', lang)} ${Math.max(...sets.map(s => s.weight || 0))}kg`;
return (
<div key={`${session.id}-${exName}`} className="flex justify-between text-sm items-center">
<span className="text-on-surface">{exName}</span>
<span className="text-on-surface-variant flex gap-2 items-center">
{detail && <span className="text-[10px] bg-surface-container-high px-2 py-0.5 rounded text-on-surface-variant">{detail}</span>}
<span>{count}</span>
</span>
</div>
);
})}
{new Set(session.sets.map(s => s.exerciseName)).size > 4 && (
<div className="text-xs text-center text-on-surface-variant mt-2">
+ ...
</div>
)}
</div>
<div className="mt-4 pt-3 border-t border-outline-variant flex justify-between items-center">
<div className="flex gap-4">
<span className="text-xs text-on-surface-variant">{t('sets_count', lang)}: <span className="text-on-surface font-medium">{session.sets.length}</span></span>
{totalWork > 0 && (
<span className="text-xs text-on-surface-variant flex items-center gap-1">
<Scale size={12} />
{(totalWork / 1000).toFixed(1)}t
</span>
)}
</div>
<div className="flex items-center gap-1 text-primary text-xs font-bold tracking-wide">
<TrendingUp size={14} />
{t('finished', lang)}
</div>
</div>
</div> </div>
) )
})} })}
@@ -251,8 +253,12 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
<span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span> <span className="w-6 h-6 rounded-full bg-secondary-container text-on-secondary-container text-xs font-bold flex items-center justify-center">{idx + 1}</span>
<span className="font-medium text-on-surface text-sm">{set.exerciseName}</span> <span className="font-medium text-on-surface text-sm">{set.exerciseName}</span>
</div> </div>
<button onClick={() => handleDeleteSet(set.id)} className="text-on-surface-variant hover:text-error"> <button
<X size={18} /> onClick={() => handleDeleteSet(set.id)}
className="text-on-surface-variant hover:text-error p-1 rounded hover:bg-error-container/10 transition-colors"
title={t('delete', lang)}
>
<Trash2 size={18} />
</button> </button>
</div> </div>
@@ -290,7 +296,39 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
/> />
</div> </div>
)} )}
{/* Add other fields similarly styled if needed */} {(set.type === ExerciseType.CARDIO || set.type === ExerciseType.STATIC) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><Timer size={10} /> {t('time_sec', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.durationSeconds ?? ''}
onChange={(e) => handleUpdateSet(set.id, 'durationSeconds', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.CARDIO || set.type === ExerciseType.LONG_JUMP) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowRight size={10} /> {t('dist_m', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.distanceMeters ?? ''}
onChange={(e) => handleUpdateSet(set.id, 'distanceMeters', parseFloat(e.target.value))}
/>
</div>
)}
{(set.type === ExerciseType.HIGH_JUMP) && (
<div className="bg-surface-container-high rounded px-2 py-1">
<label className="text-[10px] text-on-surface-variant flex gap-1 items-center"><ArrowUp size={10} /> {t('height_cm', lang)}</label>
<input
type="number"
className="w-full bg-transparent text-sm text-on-surface focus:outline-none"
value={set.height ?? ''}
onChange={(e) => handleUpdateSet(set.id, 'height', parseFloat(e.target.value))}
/>
</div>
)}
</div> </div>
</div> </div>
))} ))}

Binary file not shown.

View File

@@ -57,6 +57,8 @@ model WorkoutSession {
endTime DateTime? endTime DateTime?
userBodyWeight Float? userBodyWeight Float?
note String? note String?
planId String?
planName String?
sets WorkoutSet[] sets WorkoutSet[]
} }

View File

@@ -40,7 +40,7 @@ router.get('/', async (req: any, res) => {
router.post('/', async (req: any, res) => { router.post('/', async (req: any, res) => {
try { try {
const userId = req.user.userId; const userId = req.user.userId;
const { id, startTime, endTime, userBodyWeight, note, sets } = req.body; const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body;
// Convert timestamps to Date objects if they are numbers // Convert timestamps to Date objects if they are numbers
const start = new Date(startTime); const start = new Date(startTime);
@@ -62,6 +62,8 @@ router.post('/', async (req: any, res) => {
endTime: end, endTime: end,
userBodyWeight: weight, userBodyWeight: weight,
note, note,
planId,
planName,
sets: { sets: {
create: sets.map((s: any, idx: number) => ({ create: sets.map((s: any, idx: number) => ({
exerciseId: s.exerciseId, exerciseId: s.exerciseId,
@@ -87,6 +89,8 @@ router.post('/', async (req: any, res) => {
endTime: end, endTime: end,
userBodyWeight: weight, userBodyWeight: weight,
note, note,
planId,
planName,
sets: { sets: {
create: sets.map((s: any, idx: number) => ({ create: sets.map((s: any, idx: number) => ({
exerciseId: s.exerciseId, exerciseId: s.exerciseId,

View File

@@ -92,6 +92,7 @@ const translations = {
end_time: 'End', end_time: 'End',
max: 'Max', max: 'Max',
upto: 'Up to', upto: 'Up to',
no_plan: 'No plan',
// Plans // Plans
plans_empty: 'No plans created', plans_empty: 'No plans created',
@@ -232,6 +233,7 @@ const translations = {
end_time: 'Конец', end_time: 'Конец',
max: 'Макс', max: 'Макс',
upto: 'До', upto: 'До',
no_plan: 'Без плана',
// Plans // Plans
plans_empty: 'Нет созданных планов', plans_empty: 'Нет созданных планов',

View File

@@ -3,7 +3,13 @@ import { api } from './api';
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => { export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
try { try {
return await api.get('/sessions'); const sessions = await api.get('/sessions');
// Convert ISO date strings to timestamps
return sessions.map((session: any) => ({
...session,
startTime: new Date(session.startTime).getTime(),
endTime: session.endTime ? new Date(session.endTime).getTime() : undefined
}));
} catch { } catch {
return []; return [];
} }