Skip to content
Merged
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
23 changes: 10 additions & 13 deletions lib/database/powersync/database.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/database/powersync/tables/routines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class WorkoutLogTable extends Table {

TextColumn get id => text().clientDefault(() => ps.uuid.v7())();
IntColumn get exerciseId => integer().named('exercise_id')();
IntColumn get routineId => integer().named('routine_id')();
IntColumn get routineId => integer().named('routine_id').nullable()();
TextColumn get sessionId => text().named('session_id').nullable()();
IntColumn get iteration => integer().nullable()();
IntColumn get slotEntryId => integer().named('slot_entry_id').nullable()();
Expand Down
10 changes: 3 additions & 7 deletions lib/features/routines/models/log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Log {
late int exerciseId;
late Exercise exerciseObj;

late int routineId;
int? routineId;
String? sessionId;
int? iteration;
int? slotEntryId;
Expand All @@ -69,7 +69,7 @@ class Log {
required this.exerciseId,
this.iteration,
this.slotEntryId,
required this.routineId,
this.routineId,
this.sessionId,
this.repetitions,
this.repetitionsTarget,
Expand All @@ -84,7 +84,7 @@ class Log {
DateTime? date,
}) : date = date ?? DateTime.now();

Log.fromSetConfigData(SetConfigData setConfig, {int? routineId, this.iteration}) {
Log.fromSetConfigData(SetConfigData setConfig, {this.routineId, this.iteration}) {
date = DateTime.now();
sessionId = null;

Expand All @@ -103,10 +103,6 @@ class Log {

rir = setConfig.rir;
rirTarget = setConfig.rir;

if (routineId != null) {
this.routineId = routineId;
}
}

Log copyWith({
Expand Down
8 changes: 5 additions & 3 deletions lib/features/routines/providers/gym_state_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,11 @@ class GymStateNotifier extends _$GymStateNotifier {
return;
}

final log = Log.fromSetConfigData(slotEntryPage.setConfigData!);
log.routineId = state.routine.id!;
log.iteration = state.iteration;
final log = Log.fromSetConfigData(
slotEntryPage.setConfigData!,
routineId: state.routine.id,
iteration: state.iteration,
);
ref.read(gymLogProvider.notifier).setLog(log);
}

Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/providers/gym_state_notifier.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion lib/features/routines/providers/workout_logs_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ class WorkoutLogRepository {
final existing =
await (_db.select(_db.workoutSessionTable)
..where(
(t) => t.routineId.equals(log.routineId) & t.date.equalsValue(dayMidnightUtc),
(t) =>
t.routineId.equalsNullable(log.routineId) &
t.date.equalsValue(dayMidnightUtc),
)
..limit(1))
.getSingleOrNull();
Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/widgets/forms/session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import 'package:wger/l10n/generated/app_localizations.dart';

class SessionForm extends ConsumerStatefulWidget {
final _logger = Logger('SessionForm');
final int _routineId;
final int? _routineId;
final int? _dayId;

/// The session to edit, or null to create a new one.
Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/widgets/gym_mode/session_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class _SessionPageState extends ConsumerState<SessionPage> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: SessionForm(
gymState.routine.id!,
gymState.routine.id,
onSaved: () => widget._controller.nextPage(
duration: DEFAULT_ANIMATION_DURATION,
curve: DEFAULT_ANIMATION_CURVE,
Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/widgets/logs/session_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class _SessionInfoState extends State<SessionInfo> {
),
if (editMode)
SessionForm(
widget._session.routineId!,
widget._session.routineId,
onSaved: () => setState(() => editMode = false),
session: widget._session,
)
Expand Down
57 changes: 55 additions & 2 deletions test/features/routines/providers/workout_logs_repository_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 wger Team
* Copyright (c) 2026 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
Expand Down Expand Up @@ -39,7 +39,7 @@ void main() {

Log makeLog({
int exerciseId = 1,
int routineId = 100,
int? routineId = 100,
String? sessionId,
DateTime? date,
num weight = 50,
Expand Down Expand Up @@ -151,6 +151,44 @@ void main() {

expect(await readSessions(), hasLength(2));
});

test('creates a session without a routine for a log without a routine', () async {
final log = makeLog(routineId: null, date: DateTime.utc(2026, 4, 15, 18));

await repo.addLocalDrift(log);

final sessions = await readSessions();
expect(sessions, hasLength(1));
expect(sessions.single.routineId, isNull);
expect(log.sessionId, sessions.single.id);
});

test('reuses an existing session without a routine for a log without a routine', () async {
final existingSession = WorkoutSession(
id: 'free-session',
routineId: null,
date: DateTime.utc(2026, 4, 15),
);
await db.into(db.workoutSessionTable).insert(existingSession.toCompanion());

final log = makeLog(routineId: null, date: DateTime.utc(2026, 4, 15, 18));
await repo.addLocalDrift(log);

expect(await readSessions(), hasLength(1));
expect(log.sessionId, existingSession.id);
});

test('does not attach a log without a routine to a session with one', () async {
await db
.into(db.workoutSessionTable)
.insert(
WorkoutSession(routineId: 100, date: DateTime.utc(2026, 4, 15)).toCompanion(),
);

await repo.addLocalDrift(makeLog(routineId: null, date: DateTime.utc(2026, 4, 15)));

expect(await readSessions(), hasLength(2));
});
});

group('updateLocalDrift', () {
Expand Down Expand Up @@ -242,6 +280,21 @@ void main() {
expect(logs.single.weightUnitObj?.name, 'kg');
});

test('returns logs without a routine, e.g. free logging on the server', () async {
// The server allows logs without a routine. Such a row arrives via
// PowerSync with routine_id = NULL and must map cleanly
await seedUnits();
await db.customStatement(
'INSERT INTO manager_workoutlog (id, exercise_id, routine_id, weight, date) '
"VALUES ('free-log', 1, NULL, 50.0, '2026-04-15T10:30:00.000Z')",
);

final logs = await repo.watchLogsByExerciseDrift(exerciseId: 1).first;

expect(logs, hasLength(1));
expect(logs.single.routineId, isNull);
});

test('returns the logs of every routine when no routine is given', () async {
await seedUnits();
await db
Expand Down