Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@
"weekNr": "Week {{number}}",
"iterationNr": "Iteration {{number}}",
"backToRoutine": "Back to routine",
"lastSession": "Last session",
"minLengthRoutine": "The routine needs to be at least {{number}} weeks long",
"maxLengthRoutine": "The routine can be at most {{number}} weeks long",
"resultingRoutine": "Resulting routine",
Expand Down
62 changes: 62 additions & 0 deletions src/components/Routines/models/WorkoutLog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { WorkoutLog, WorkoutLogAdapter } from "@/components/Routines/models/WorkoutLog";

const adapter = new WorkoutLogAdapter();

const baseApiResponse = {
id: 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001',
iteration: 1,
date: "2024-08-01",
exercise: 100,
slot_entry: 2,
routine: 1,
session: null,
repetitions_unit: 1,
repetitions: "10.00",
repetitions_target: null,
weight_unit: 1,
weight: "20.00",
weight_target: null,
rir: null,
rir_target: null,
rest: null,
rest_target: null,
};

describe('WorkoutLog adapter', () => {
test('fromJson sets notes when present', () => {
const log = adapter.fromJson({ ...baseApiResponse, notes: 'Felt strong' });
expect(log.notes).toBe('Felt strong');
});

test('fromJson sets notes to null when absent', () => {
const log = adapter.fromJson(baseApiResponse);
expect(log.notes).toBeNull();
});

test('toJson includes notes', () => {
const log = adapter.fromJson({ ...baseApiResponse, notes: 'Heavy day' });
const json = adapter.toJson(log);
expect(json.notes).toBe('Heavy day');
});

test('toJson sets notes to null when not set', () => {
const log = adapter.fromJson(baseApiResponse);
const json = adapter.toJson(log);
expect(json.notes).toBeNull();
});

test('constructor accepts notes', () => {
const log = new WorkoutLog({
id: 'aaaaaaaa-aaaa-aaaa-aaaa-000000000001',
date: new Date('2024-08-01'),
iteration: 1,
exerciseId: 100,
slotEntryId: 2,
repetitions: 10,
weight: 20,
rir: null,
notes: 'Test note',
});
expect(log.notes).toBe('Test note');
});
});
13 changes: 10 additions & 3 deletions src/components/Routines/models/WorkoutLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface LogEntryForm {
repetitionsTarget: number | string | null;
weight: number | string;
weightTarget: number | string | null;
notes: string;
}


Expand Down Expand Up @@ -44,6 +45,7 @@ export class WorkoutLog {
public restTime: number | null;
public restTimeTarget: number | null;

public notes: string | null;

public exerciseObj?: Exercise;

Expand Down Expand Up @@ -72,7 +74,9 @@ export class WorkoutLog {
rirTarget?: number | null;

restTime?: number | null;
restTimeTarget?: number | null
restTimeTarget?: number | null;

notes?: string | null;
}) {
this.id = data.id;
this.date = typeof data.date === 'string' ? new Date(data.date) : data.date;
Expand All @@ -99,6 +103,7 @@ export class WorkoutLog {

this.restTime = data.restTime || null;
this.restTimeTarget = data.restTimeTarget || null;
this.notes = data.notes || null;
}

get rirString(): string {
Expand Down Expand Up @@ -131,7 +136,8 @@ export class WorkoutLogAdapter implements Adapter<WorkoutLog> {
rirTarget: item.rir_target === null ? null : Number.parseFloat(item.rir_target),

restTime: item.rest,
restTimeTarget: item.rest_target
restTimeTarget: item.rest_target,
notes: item.notes || null
});

toJson = (item: WorkoutLog) => ({
Expand All @@ -154,6 +160,7 @@ export class WorkoutLogAdapter implements Adapter<WorkoutLog> {
rir_target: item.rirTarget,

rest: item.restTime,
rest_target: item.restTimeTarget
rest_target: item.restTimeTarget,
notes: item.notes || null
});
}
10 changes: 10 additions & 0 deletions src/components/Routines/widgets/LogWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const ExerciseLog = (props: { exercise: Exercise, routineId: number, logE
repetitions: logEntry.repetitions,
weight: logEntry.weight,
rir: logEntry.rir,
notes: logEntry.notes,
entry: logEntry
}));

Expand Down Expand Up @@ -98,6 +99,7 @@ export const ExerciseLog = (props: { exercise: Exercise, routineId: number, logE
log.repetitions = newRow.repetitions;
log.weight = newRow.weight;
log.rir = newRow.rir;
log.notes = newRow.notes;

editLogQuery.mutate(log);
}
Expand Down Expand Up @@ -153,6 +155,14 @@ export const ExerciseLog = (props: { exercise: Exercise, routineId: number, logE
getOptionLabel: (value: any) => value.label,
valueOptions: RIR_VALUES_SELECT_LIST,
},
{
field: 'notes',
type: 'string',
flex: 2,
disableColumnMenu: true,
editable: true,
headerName: t('notes'),
},
{
field: 'actions',
type: 'actions',
Expand Down
58 changes: 57 additions & 1 deletion src/components/Routines/widgets/forms/SessionLogsForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { render, screen } from '@testing-library/react';
import userEvent from "@testing-library/user-event";
import { useLanguageQuery } from "@/components/Exercises";
import { useAddRoutineLogsQuery, useRoutineDetailQuery } from "@/components/Routines/queries";
import { useAddRoutineLogsQuery, useRoutineDetailQuery, useRoutineLogQuery } from "@/components/Routines/queries";
import { SessionLogsForm } from '@/components/Routines/widgets/forms/SessionLogsForm';
import { DateTime } from "luxon";
import { testLanguages } from "@/tests/exerciseTestdata";
import { testRoutine1 } from "@/tests/workoutRoutinesTestData";
import { testWorkoutLogs } from "@/tests/workoutLogsRoutinesTestData";
import { WorkoutLog } from "@/components/Routines/models/WorkoutLog";
import type { Mock } from 'vitest';


Expand All @@ -17,6 +19,7 @@ describe('SessionLogsForm', () => {
const mockUseLanguageQuery = useLanguageQuery as Mock;
const mockAddLogsQuery = useAddRoutineLogsQuery as Mock;
const mockRoutineDetailQuery = useRoutineDetailQuery as Mock;
const mockRoutineLogQuery = useRoutineLogQuery as Mock;
const mockMutateAsync = vi.fn();

beforeEach(() => {
Expand All @@ -34,6 +37,10 @@ describe('SessionLogsForm', () => {
isLoading: false,
data: testLanguages,
});
mockRoutineLogQuery.mockReturnValue({
isSuccess: false,
data: [],
});
});


Expand Down Expand Up @@ -124,4 +131,53 @@ describe('SessionLogsForm', () => {
// Assert
expect(screen.queryByText('Squats')).not.toBeInTheDocument();
});

test('renders notes field for each log entry', async () => {
render(<SessionLogsForm
dayId={5}
routineId={1}
selectedDate={DateTime.now()}
/>);

const notesFields = screen.getAllByRole('textbox', { name: /notes/i });
expect(notesFields.length).toBeGreaterThan(0);
});

test('shows last-session note as helper text when previous log has notes', async () => {
const logWithNote = new WorkoutLog({
...testWorkoutLogs[0],
exerciseId: 345,
notes: 'Felt great last time',
});
mockRoutineLogQuery.mockReturnValue({
isSuccess: true,
data: [logWithNote],
});

render(<SessionLogsForm
dayId={5}
routineId={1}
selectedDate={DateTime.now()}
/>);

expect(screen.getAllByText(/Felt great last time/).length).toBeGreaterThan(0);
});

test('submits notes value with log entry', async () => {
const user = userEvent.setup();

render(<SessionLogsForm
dayId={5}
routineId={1}
selectedDate={DateTime.fromISO('2024-05-05T12:00:00')}
/>);

const notesField = screen.getAllByRole('textbox', { name: /notes/i })[0];
await user.click(notesField);
await user.type(notesField, 'Good session');
await user.click(screen.getByRole('button', { name: /submit/i }));

expect(mockMutateAsync).toHaveBeenCalled();
expect(mockMutateAsync.mock.calls[0][0][0]).toMatchObject({ notes: 'Good session' });
});
});
31 changes: 29 additions & 2 deletions src/components/Routines/widgets/forms/SessionLogsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LoadingPlaceholder } from "@/core/ui/LoadingWidget/LoadingWidget";
import { Exercise, getLanguageByShortName, NameAutocompleter, useLanguageQuery } from "@/components/Exercises";
import { RIR_VALUES_SELECT } from "@/components/Routines/models/BaseConfig";
import { LogEntryForm } from "@/components/Routines/models/WorkoutLog";
import { useAddRoutineLogsQuery, useRoutineDetailQuery } from "@/components/Routines/queries";
import { useAddRoutineLogsQuery, useRoutineDetailQuery, useRoutineLogQuery } from "@/components/Routines/queries";
import { REP_UNIT_REPETITIONS, SNACKBAR_AUTO_HIDE_DURATION } from "@/core/lib/consts";
import { SwapHoriz } from "@mui/icons-material";
import AddIcon from "@mui/icons-material/Add";
Expand All @@ -28,6 +28,7 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
const [snackbarOpen, setSnackbarOpen] = useState(false);
const routineQuery = useRoutineDetailQuery(routineId);
const addLogsQuery = useAddRoutineLogsQuery(routineId);
const logsQuery = useRoutineLogQuery(routineId);
const languageQuery = useLanguageQuery();
const handleSnackbarClose = () => setSnackbarOpen(false);
const [exerciseIdToSwap, setExerciseIdToSwap] = useState<number | null>(null);
Expand All @@ -40,6 +41,17 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
);
}

// Build a map of exerciseId → most recent note from previous logs
const lastNoteByExercise = new Map<number, string>();
if (logsQuery.isSuccess) {
const sorted = [...logsQuery.data].sort((a, b) => b.date.getTime() - a.date.getTime());
for (const log of sorted) {
if (log.notes && !lastNoteByExercise.has(log.exerciseId)) {
lastNoteByExercise.set(log.exerciseId, log.notes);
}
}
}

if (routineQuery.isLoading) {
return <LoadingPlaceholder />;
}
Expand Down Expand Up @@ -86,6 +98,8 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
weight: l.weight !== '' ? l.weight : null,
// eslint-disable-next-line camelcase
weight_target: l.weightTarget !== '' ? l.weightTarget : null,

notes: l.notes !== '' ? l.notes : null,
}
));

Expand Down Expand Up @@ -149,7 +163,8 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
repetitions: !hasNoIterationData && config.repetitions !== null ? config.repetitions : '',
repetitionsTarget: !hasNoIterationData && config.repetitions !== null ? config.repetitions : '',
weight: !hasNoIterationData && config.weight !== null ? config.weight : '',
weightTarget: !hasNoIterationData && config.weight !== null ? config.weight : ''
weightTarget: !hasNoIterationData && config.weight !== null ? config.weight : '',
notes: ''
});
}
}
Expand Down Expand Up @@ -292,6 +307,18 @@ export const SessionLogsForm = ({ dayId, routineId, selectedDate }: SessionLogsF
))}
</TextField>
</Grid>
<Grid size={11}>
<TextField
fullWidth
label={t('notes')}
variant="standard"
multiline
helperText={lastNoteByExercise.get(formik.values.logs[index].exercise?.id ?? 0)
? `${t('routines.lastSession')}: ${lastNoteByExercise.get(formik.values.logs[index].exercise?.id ?? 0)}`
: undefined}
{...formik.getFieldProps(`logs.${index}.notes`)}
/>
</Grid>
<Grid size={1}>
<IconButton size={"small"} onClick={() => remove(index)}>
<DeleteIcon />
Expand Down