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-ssr
*.local
server/prisma/dev.db
*.db
# Editor directories and files
.vscode/*

View File

@@ -41,6 +41,23 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
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 = () => {
if (editingSession && onUpdateSession) {
onUpdateSession(editingSession);
@@ -91,81 +108,66 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
const totalWork = calculateSessionWork(session);
return (
<div key={session.id} className="bg-surface-container rounded-xl p-5 shadow-elevation-1 border border-outline-variant/20">
<div className="flex justify-between items-start mb-4 border-b border-outline-variant pb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-tertiary-container text-on-tertiary-container flex items-center justify-center">
<Calendar size={20} />
<div
key={session.id}
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"
onClick={() => setEditingSession(JSON.parse(JSON.stringify(session)))}
>
<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 className="font-medium text-on-surface text-lg">
{new Date(session.startTime).toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { weekday: 'long', day: 'numeric', month: 'long' })}
</div>
<div className="text-xs text-on-surface-variant flex items-center gap-2">
<span>{new Date(session.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
{session.userBodyWeight && <span className="px-2 py-0.5 rounded-full bg-surface-container-high text-on-surface">{session.userBodyWeight}kg</span>}
</div>
<div className="mt-2 text-xs text-on-surface-variant">
{t('sets_count', lang)}: <span className="text-on-surface font-medium">{session.sets.length}</span>
{totalWork > 0 && (
<span className="ml-4 inline-flex items-center gap-1">
<Scale size={12} />
{(totalWork / 1000).toFixed(1)}t
</span>
)}
</div>
</div>
<div className="flex gap-1">
<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"
>
<Pencil size={20} />
</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"
>
<Trash2 size={20} />
</button>
</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>
)
})}
@@ -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="font-medium text-on-surface text-sm">{set.exerciseName}</span>
</div>
<button onClick={() => handleDeleteSet(set.id)} className="text-on-surface-variant hover:text-error">
<X size={18} />
<button
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>
</div>
@@ -290,7 +296,39 @@ const History: React.FC<HistoryProps> = ({ sessions, onUpdateSession, onDeleteSe
/>
</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>
))}

Binary file not shown.

View File

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

View File

@@ -40,7 +40,7 @@ router.get('/', async (req: any, res) => {
router.post('/', async (req: any, res) => {
try {
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
const start = new Date(startTime);
@@ -62,6 +62,8 @@ router.post('/', async (req: any, res) => {
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
sets: {
create: sets.map((s: any, idx: number) => ({
exerciseId: s.exerciseId,
@@ -87,6 +89,8 @@ router.post('/', async (req: any, res) => {
endTime: end,
userBodyWeight: weight,
note,
planId,
planName,
sets: {
create: sets.map((s: any, idx: number) => ({
exerciseId: s.exerciseId,

View File

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

View File

@@ -3,7 +3,13 @@ import { api } from './api';
export const getSessions = async (userId: string): Promise<WorkoutSession[]> => {
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 {
return [];
}