From 54cd9158189d50d12e683d3e415134f4cdd9ab2d Mon Sep 17 00:00:00 2001 From: AG Date: Wed, 17 Dec 2025 00:44:12 +0200 Subject: [PATCH] Create Plan from Session. Top bar rounded --- server/prisma/dev.db | Bin 106496 -> 139264 bytes specs/gymflow-test-plan.md | 16 ++++ specs/requirements.md | 12 ++- src/components/History.tsx | 94 ++++++++++++++++--- src/components/Plans.tsx | 39 +++++++- src/components/ui/TopBar.tsx | 2 +- src/services/i18n.ts | 2 + tailwind.config.js | 11 ++- tests/plan-from-session.spec.ts | 158 ++++++++++++++++++++++++++++++++ 9 files changed, 313 insertions(+), 21 deletions(-) create mode 100644 tests/plan-from-session.spec.ts diff --git a/server/prisma/dev.db b/server/prisma/dev.db index b22e9e393f3b2ffb517b525ae5843d254df906df..aa07a4722293694afc6e883e476ad8cf569c204b 100644 GIT binary patch literal 139264 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 delta 417 zcmZoTz|nAkZGtpwJp%)S)I2{$3nm=rpSZx7jSHv^!R6uw@)-G7GVreis+i9|d1ZYxSnK3F{VMDrNq#Q& z>Ff&_!XGFfnz%H~6BA2Uw=FprawdHVb7jG8R!%%Pl<=gwDQ16hJ_5SsfS zPT=I2%zgpnf=3*iId1-8Qvh3T2ef(u(4sazQLyDVfL5>P=jM9^v|7NS3~KdFpw(}k zjBp3RHU~x)0buaGVc>rQ4JGO6Z{isxkhE+{xC0AJR&L&73_!#WbTuE)i`#g&AM0hb LV}gX5;B6HEFt2_T diff --git a/specs/gymflow-test-plan.md b/specs/gymflow-test-plan.md index facd3e4..a7f700e 100644 --- a/specs/gymflow-test-plan.md +++ b/specs/gymflow-test-plan.md @@ -204,6 +204,22 @@ Comprehensive test plan for the GymFlow web application, covering authentication - The session starts with the selected plan's exercises. - The timer starts running. +#### 2.5a. A. Workout Plans - Create Plan from Session + +**File:** `tests/plan-from-session.spec.ts` + +**Steps:** + 1. User completes a session with multiple sets (e.g., 2 sets of Pushups, 1 set of Squats). + 2. Navigate to 'History'. + 3. Click 'Create Plan' from the session menu. + 4. Verify the Plan Editor opens. + 5. **Verify Steps**: The plan should contain exactly 3 steps (Pushups, Pushups, Squats). + +**Expected Results:** + - The Plan Editor is pre-filled. + - Plan steps mirror the session sets 1:1. + + #### 2.6. B. Exercise Library - Create Custom Exercise (Strength) **File:** `tests/workout-management.spec.ts` diff --git a/specs/requirements.md b/specs/requirements.md index 7c34d20..bfcf393 100644 --- a/specs/requirements.md +++ b/specs/requirements.md @@ -64,7 +64,17 @@ Users can structure their training via Plans. * **Logic**: Supports reordering capabilities via drag-and-drop in UI. * **3.2.2 Plan Deletion** * Standard soft or hard delete (Cascades to PlanExercises). -* **3.2.3 AI Plan Creation** +* **3.2.3 Create Plan from Session** + * **Trigger**: Action menu in History session. + * **Logic**: + * Creates a new Plan pre-filled with data from the selected session. + * **Step Generation**: Mirrors sets 1:1. Every recorded set in the session becomes a distinct step in the plan (no collapsing). + * **Attributes**: + * `startWeight`: inherited from set. + * `restTime`: uses User's default rest timer setting. + * `isWeighted`: true if the specific set had weight > 0. + +* **3.2.4 AI Plan Creation** * **Trigger**: "Create with AI" option in Plans FAB Menu, or "Ask your AI coach" link from Tracker (when no plans exist). * **UI Flow**: * Opens a dedicated Side Sheet in the Plans view. diff --git a/src/components/History.tsx b/src/components/History.tsx index 602efc9..0e9ebe2 100644 --- a/src/components/History.tsx +++ b/src/components/History.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; -import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save } from 'lucide-react'; +import { createPortal } from 'react-dom'; +import { useNavigate } from 'react-router-dom'; +import { Trash2, Calendar, Clock, ChevronDown, ChevronUp, History as HistoryIcon, Dumbbell, Ruler, Timer, Weight, Edit2, Gauge, Pencil, Save, MoreVertical, ClipboardList } from 'lucide-react'; import { TopBar } from './ui/TopBar'; import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types'; import { t } from '../services/i18n'; @@ -9,6 +11,7 @@ import { useSession } from '../context/SessionContext'; import { useAuth } from '../context/AuthContext'; import { getExercises } from '../services/storage'; import { Button } from './ui/Button'; +import { Ripple } from './ui/Ripple'; import { Card } from './ui/Card'; import { Modal } from './ui/Modal'; import { SideSheet } from './ui/SideSheet'; @@ -23,9 +26,11 @@ const History: React.FC = ({ lang }) => { const { sessions, updateSession, deleteSession } = useSession(); const { currentUser } = useAuth(); const userId = currentUser?.id || ''; + const navigate = useNavigate(); const [exercises, setExercises] = useState([]); const [editingSession, setEditingSession] = useState(null); + const [menuState, setMenuState] = useState<{ id: string, x: number, y: number } | null>(null); const [deletingId, setDeletingId] = useState(null); const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null); @@ -221,28 +226,23 @@ const History: React.FC = ({ lang }) => { -
+
-
@@ -328,6 +328,70 @@ const History: React.FC = ({ lang }) => { + {/* MENU PORTAL */} + {menuState && typeof document !== 'undefined' && createPortal( + <> +
{ + e.stopPropagation(); + setMenuState(null); + }} + /> +
+ + + +
+ , + document.body + )} + {/* DELETE CONFIRMATION MODAL */} = ({ lang }) => { setShowAISheet(true); setSearchParams({}); } - }, [searchParams, setSearchParams]); + + const sourceSessionId = searchParams.get('createFromSessionId'); + if (sourceSessionId && sessions.length > 0) { + const sourceSession = sessions.find(s => s.id === sourceSessionId); + if (sourceSession) { + handleCreateNew(); + + // Generate name + const dateStr = new Date(sourceSession.startTime).toLocaleDateString(); + setName(sourceSession.planName || (lang === 'ru' ? `План от ${dateStr}` : `Plan from ${dateStr}`)); + if (sourceSession.note) setDescription(sourceSession.note); + + // Generate steps from sets + const newSteps: PlannedSet[] = []; + let lastExerciseId: string | null = null; + + // Use default rest timer or 60s + const defaultRest = currentUser?.profile?.restTimerDefault || 60; + + sourceSession.sets.forEach(set => { + // Mirror every set from the session to the plan + newSteps.push({ + id: generateId(), + exerciseId: set.exerciseId, + exerciseName: set.exerciseName, + exerciseType: set.type, + isWeighted: (set.weight || 0) > 0, + restTimeSeconds: defaultRest + }); + }); + + setSteps(newSteps); + + // Clear param so we don't re-run + setSearchParams({}); + } + } + }, [searchParams, setSearchParams, sessions, currentUser]); const handleStart = (plan: WorkoutPlan) => { if (plan.description && plan.description.trim().length > 0) { diff --git a/src/components/ui/TopBar.tsx b/src/components/ui/TopBar.tsx index f0493ce..e95b526 100644 --- a/src/components/ui/TopBar.tsx +++ b/src/components/ui/TopBar.tsx @@ -9,7 +9,7 @@ interface TopBarProps { export const TopBar: React.FC = ({ title, icon: Icon, actions }) => { return ( -
+
{Icon && (
diff --git a/src/services/i18n.ts b/src/services/i18n.ts index e74ded4..e6465b3 100644 --- a/src/services/i18n.ts +++ b/src/services/i18n.ts @@ -111,6 +111,7 @@ const translations = { max: 'Max', upto: 'Up to', no_plan: 'No plan', + create_plan: 'Create Plan', // Plans plans_empty: 'No plans created', @@ -325,6 +326,7 @@ const translations = { max: 'Макс', upto: 'До', no_plan: 'Без плана', + create_plan: 'Создать план', // Plans plans_empty: 'Нет созданных планов', diff --git a/tailwind.config.js b/tailwind.config.js index 292cab5..8f1a005 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -63,15 +63,20 @@ export default { 'elevation-4': '0px 2px 3px rgba(0, 0, 0, 0.3), 0px 6px 10px 4px rgba(0, 0, 0, 0.15)', 'elevation-5': '0px 4px 4px rgba(0, 0, 0, 0.3), 0px 8px 12px 6px rgba(0, 0, 0, 0.15)', }, + animation: { + ripple: 'ripple 600ms linear', + 'menu-enter': 'menu-enter 200ms ease-out forwards', + }, keyframes: { ripple: { '0%': { transform: 'scale(0)', opacity: '0.4' }, '100%': { transform: 'scale(4)', opacity: '0' }, + }, + 'menu-enter': { + '0%': { opacity: '0', transform: 'scale(0.95) translateX(-100%)' }, // maintain the translate relative to position + '100%': { opacity: '1', transform: 'scale(1) translateX(-100%)' }, } }, - animation: { - ripple: 'ripple 600ms linear', - } }, }, plugins: [], diff --git a/tests/plan-from-session.spec.ts b/tests/plan-from-session.spec.ts new file mode 100644 index 0000000..2dde3eb --- /dev/null +++ b/tests/plan-from-session.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from './fixtures'; +import { generateId } from '../src/utils/uuid'; +import { ExerciseType } from '../src/types'; + +test('Create Plan from Session mirrors all sets 1:1', async ({ page, request, createUniqueUser }) => { + // 1. Setup User + const user = await createUniqueUser(); + + // 2. Create Exercises + const pushupsId = generateId(); + const squatsId = generateId(); + + // Directly seed exercises via API (assuming a helper or just DB seed if possible, + // but here we might need to use the app or just mock the session data if we can inject it? + // Actually, createUniqueUser returns a token. We can use it to POST /exercises if that endpoint exists, + // or just rely on 'default' exercises if they are seeded. + // Let's use the 'saveSession' endpoint directly logic if we can, or just mock the DB state. + // Wait, the app uses local storage mostly or sync? + // Based on other tests (which I can't read right now but recall structure), they usually use UI or API helpers. + // I will assume I can just Login and then use UI or API. + // Let's use UI to just ensure clean state, or API `POST /sessions` if available. + // Based on `server/src/routes/sessions.ts` existing, I can POST session. + + // Let's rely on standard UI flows or API. + // API is faster. + + const token = user.token; + + // Create Custom Exercises via API + await request.post('http://localhost:3000/api/exercises', { + headers: { Authorization: `Bearer ${token}` }, + data: { + id: pushupsId, + name: 'Test Pushups', + type: 'BODYWEIGHT', + isUnilateral: false + } + }); + + await request.post('http://localhost:3000/api/exercises', { + headers: { Authorization: `Bearer ${token}` }, + data: { + id: squatsId, + name: 'Test Squats', + type: 'STRENGTH', + isUnilateral: false + } + }); + + // 3. Create Session with 3 sets (A, A, B) + const sessionId = generateId(); + const sessionData = { + id: sessionId, + startTime: Date.now() - 3600000, // 1 hour ago + endTime: Date.now(), + note: 'Killer workout', + type: 'STANDARD', + sets: [ + { + id: generateId(), + exerciseId: pushupsId, + exerciseName: 'Test Pushups', + type: 'BODYWEIGHT', + reps: 10, + timestamp: Date.now() - 3000000, + completed: true + }, + { + id: generateId(), + exerciseId: pushupsId, + exerciseName: 'Test Pushups', + type: 'BODYWEIGHT', + reps: 12, + weight: 10, // Weighted + timestamp: Date.now() - 2000000, + completed: true + }, + { + id: generateId(), + exerciseId: squatsId, + exerciseName: 'Test Squats', + type: 'STRENGTH', + reps: 5, + weight: 100, + timestamp: Date.now() - 1000000, + completed: true + } + ] + }; + + await request.post('http://localhost:3000/api/sessions', { + headers: { Authorization: `Bearer ${token}` }, + data: sessionData + }); + + // 4. Login and Navigate + await page.goto('http://localhost:3000/'); + await page.fill('input[type="email"]', user.email); + await page.fill('input[type="password"]', user.password); + await page.click('button:has-text("Login")'); + await page.waitForURL('**/tracker'); + + // 5. Go to History + await page.click('text=History'); + + // 6. Click Create Plan + const sessionCard = page.locator('div.bg-surface-container').first(); // Assuming it's the first card + await sessionCard.waitFor(); + + // Open Menu + await sessionCard.locator('button[aria-label="Session Actions"]').click(); + // Click 'Create Plan' + await page.click('text=Create Plan'); + + // 7. Verify Redirection + await expect(page).toHaveURL(/.*plans\?createFromSessionId=.*/); + + // 8. Verify Plan Editor Content + await expect(page.locator('h2')).toContainText('Plan Editor'); + + // Name should be "Plan from [Date]" or Session Name + // Note: Session had no planName, so it defaults to date. + // But we can check the Description matches 'Killer workout' + await expect(page.locator('textarea')).toHaveValue('Killer workout'); + + // 9. Verify 3 Steps (1:1 mapping) + // We expect 3 cards in the sortable list + const steps = page.locator('.dnd-sortable-item_content, div[class*="items-center"] > div.flex-1'); + // Finding a robust selector for steps is tricky without specific test ids. + // The SortablePlanStep component has `div.text-base.font-medium.text-on-surface` for exercise name. + + const stepNames = page.locator('div.text-base.font-medium.text-on-surface'); + await expect(stepNames).toHaveCount(3); + + await expect(stepNames.nth(0)).toHaveText('Test Pushups'); + await expect(stepNames.nth(1)).toHaveText('Test Pushups'); + await expect(stepNames.nth(2)).toHaveText('Test Squats'); + + // 10. Verify Weighted Flag Logic + // Set 1 (index 0): Unweighted + // Set 2 (index 1): Weighted (weight: 10) + // Set 3 (index 2): Weighted (weight: 100) + + const checkboxes = page.locator('input[type="checkbox"]'); + // Warning: there might be other checkboxes. + // SortablePlanStep has a checkbox for 'weighted'. + // Better to look for checked state within the step card. + + // Step 1: Unchecked + await expect(page.locator('input[type="checkbox"]').nth(0)).not.toBeChecked(); + + // Step 2: Checked + await expect(page.locator('input[type="checkbox"]').nth(1)).toBeChecked(); + + // Step 3: Checked + await expect(page.locator('input[type="checkbox"]').nth(2)).toBeChecked(); + +});