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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Workout details feature for authenticated users, including `GET /api/workout-execution/{userWorkout}`, details repository, Cubit, route, and the details UI based on the provided mockup.
- Authenticated tests catalog screen for the `/tests` root tab, reusing the existing tests catalog slice and adding local search plus multi-select category filtering UI before navigating to the debug screen.
- Workout execution feature for authenticated users, including execution DTOs/mappers, repository, Cubit, fullscreen route, local rest countdown, dialogs, and the warmup/training UI backed by workout start/progression/complete endpoints.
- Authenticated test attempt flow for `/tests/attempt/:testingId`, including auth API client methods, repository wiring, fullscreen attempt route, and the attempt UI mirrored from the Fitness Start flow.

### Changed

Expand All @@ -25,8 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Workout execution now captures used weight before saving exercise results, shows load-adjustment feedback from backend, and displays exercise instructions with sets, reps, and current weight.
- Workouts overview and details screens now reuse a shared `WorkoutCard` widget instead of maintaining duplicated card implementations.
- Workouts overview and details app bars now reuse a dedicated `appBarTitle` text token instead of local per-page style overrides.
- Authenticated tests catalog cards now open the real test attempt flow instead of the debug screen.
- `TestingCatalogCard` now skips the extra spacing above category chips when a test has no categories.

### Breaking

- Shared test-attempt transport DTOs were renamed from guest-prefixed names to neutral request/response models because the same payload shapes are now reused by both guest and authenticated flows.
- Test-attempt DI wiring now resolves separate guest and authenticated repository bindings while keeping the shared `TestAttemptCubit` and domain contract unchanged.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## [0.3.1] - 2026-03-25

### Added
Expand Down
9 changes: 8 additions & 1 deletion lib/core/di/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import '../../features/fitness_start/data/remote/fitness_start_api_client.dart';
import '../../features/fitness_start/data/repositories/fitness_start_repository_impl.dart';
import '../../features/fitness_start/domain/repositories/fitness_start_repository.dart';
import '../../features/offline/presentation/cubit/network_cubit.dart';
import '../../features/tests/attempt/data/repositories/authenticated_test_attempt_repository_impl.dart';
import '../../features/tests/attempt/data/repositories/guest_test_attempt_repository_impl.dart';
import '../../features/tests/attempt/domain/repositories/test_attempt_repository.dart';
import '../../features/tests/catalog/data/repositories/tests_catalog_repository_impl.dart';
Expand Down Expand Up @@ -139,12 +140,18 @@ Future<void> setupDI() async {
di<TestsApiClient>(),
),
);
di.registerLazySingleton<TestAttemptRepository>(
di.registerLazySingleton<GuestTestAttemptRepository>(
() => GuestTestAttemptRepositoryImpl(
di<AppLogger>(),
di<TestsApiClient>(),
),
);
di.registerLazySingleton<AuthenticatedTestAttemptRepository>(
() => AuthenticatedTestAttemptRepositoryImpl(
di<AppLogger>(),
di<TestsApiClient>(),
),
);

// Workouts
di.registerLazySingleton<WorkoutsApiClient>(() => WorkoutsApiClient(di<Dio>()));
Expand Down
6 changes: 6 additions & 0 deletions lib/core/network/api_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ abstract class ApiPaths {
/// The endpoint for all active testings.
static const String testings = '${apiPrefix}testings';

/// The endpoint prefix for authenticated tests.
static const String tests = '${apiPrefix}tests';

/// The endpoint prefix for authenticated test attempts.
static const String testAttempts = '${apiPrefix}test-attempts';

/// The endpoint prefix for guest tests.
static const String guestTests = '${apiPrefix}guest/tests';

Expand Down
17 changes: 17 additions & 0 deletions lib/core/router/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import '../../features/offline/presentation/cubit/network_cubit.dart';
import '../../features/offline/presentation/pages/offline_page.dart';
import '../../features/root/presentation/pages/root_screen.dart';
import '../../features/splash/presentation/pages/splash_page.dart';
import '../../features/tests/attempt/presentation/pages/tests_attempt_page_builder.dart';
import '../../features/tests/catalog/presentation/pages/tests_catalog_page_builder.dart';
import '../../features/workouts/details/presentation/pages/workout_details_page_builder.dart';
import '../../features/workouts/execution/domain/entities/workout_execution_entry_mode.dart';
Expand Down Expand Up @@ -178,6 +179,22 @@ final router = GoRouter(
GoRoute(
path: AppRoutePaths.testsPath,
builder: (_, _) => const TestsCatalogPageBuilder(),
routes: [
GoRoute(
path: AppRoutePaths.testsAttemptPath,
parentNavigatorKey: _rootKey,
redirect: (_, state) {
final testingId = int.tryParse(state.pathParameters['testingId'] ?? '');
if (testingId == null || testingId <= 0) {
return AppRoutePaths.testsPath;
}
return null;
},
builder: (_, state) => TestsAttemptPageBuilder(
testingId: int.parse(state.pathParameters['testingId']!),
),
),
],
),
],
),
Expand Down
6 changes: 6 additions & 0 deletions lib/core/router/router_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ abstract class AppRoutePaths {
/// Route path for the tests root tab.
static const testsPath = '/tests';

/// Route path pattern for a concrete authenticated test attempt.
static const testsAttemptPath = 'attempt/:testingId';

/// Builds the concrete route path for an authenticated test attempt by [testingId].
static String testsAttemptDetailsPath(int testingId) => '$testsPath/attempt/$testingId';

/// Route path for the profile root tab.
static const profilePath = '/profile';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FitnessStartTestAttemptPageBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TestAttemptCubit(di<TestAttemptRepository>())..startTest(testingId),
create: (_) => TestAttemptCubit(di<GuestTestAttemptRepository>())..startTest(testingId),
child: FitnessStartTestAttemptPage(testingId: testingId),
);
}
Expand Down

This file was deleted.

16 changes: 16 additions & 0 deletions lib/features/tests/attempt/data/dto/complete_test_request_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:json_annotation/json_annotation.dart';

part 'complete_test_request_dto.g.dart';

/// DTO request for completing a test attempt.
@JsonSerializable()
class CompleteTestRequestDto {
/// Pulse after finishing the testing.
final int pulse;

/// Creates an instance of [CompleteTestRequestDto].
CompleteTestRequestDto({required this.pulse});

/// Converts this request to JSON.
Map<String, dynamic> toJson() => _$CompleteTestRequestDtoToJson(this);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import 'package:json_annotation/json_annotation.dart';

import 'testing_exercise_dto.dart';

part 'save_guest_test_result_data_dto.g.dart';
part 'save_test_result_data_dto.g.dart';

/// DTO payload returned after saving guest exercise result.
/// DTO payload returned after saving exercise result.
@JsonSerializable(createToJson: false)
class SaveGuestTestResultDataDto {
class SaveTestResultDataDto {
/// Whether the result was saved successfully.
final bool saved;

Expand All @@ -18,14 +18,14 @@ class SaveGuestTestResultDataDto {
@JsonKey(name: 'all_exercises_completed')
final bool? allExercisesCompleted;

/// Creates an instance of [SaveGuestTestResultDataDto].
SaveGuestTestResultDataDto({
/// Creates an instance of [SaveTestResultDataDto].
SaveTestResultDataDto({
required this.saved,
required this.nextExercise,
required this.allExercisesCompleted,
});

/// Creates a [SaveGuestTestResultDataDto] from JSON.
factory SaveGuestTestResultDataDto.fromJson(Map<String, dynamic> json) =>
_$SaveGuestTestResultDataDtoFromJson(json);
/// Creates a [SaveTestResultDataDto] from JSON.
factory SaveTestResultDataDto.fromJson(Map<String, dynamic> json) =>
_$SaveTestResultDataDtoFromJson(json);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:json_annotation/json_annotation.dart';

part 'save_test_result_request_dto.g.dart';

/// DTO request for saving test exercise result.
@JsonSerializable()
class SaveTestResultRequestDto {
/// Exercise identifier inside the testing.
@JsonKey(name: 'testing_exercise_id')
final int testingExerciseId;

/// Result value from `1` to `4`.
@JsonKey(name: 'result_value')
final int resultValue;

/// Creates an instance of [SaveTestResultRequestDto].
SaveTestResultRequestDto({
required this.testingExerciseId,
required this.resultValue,
});

/// Converts this request to JSON.
Map<String, dynamic> toJson() => _$SaveTestResultRequestDtoToJson(this);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:json_annotation/json_annotation.dart';

import 'save_test_result_data_dto.dart';

part 'save_test_result_response_dto.g.dart';

/// DTO envelope for saving test exercise result.
@JsonSerializable(createToJson: false)
class SaveTestResultResponseDto {
/// Saved test result payload.
final SaveTestResultDataDto data;

/// Creates an instance of [SaveTestResultResponseDto].
SaveTestResultResponseDto({required this.data});

/// Creates a [SaveTestResultResponseDto] from JSON.
factory SaveTestResultResponseDto.fromJson(Map<String, dynamic> json) =>
_$SaveTestResultResponseDtoFromJson(json);
}
31 changes: 31 additions & 0 deletions lib/features/tests/attempt/data/dto/start_test_data_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:json_annotation/json_annotation.dart';

import 'test_attempt_testing_dto.dart';
import 'testing_exercise_dto.dart';

part 'start_test_data_dto.g.dart';

/// DTO payload returned when an authenticated test attempt is started.
@JsonSerializable(createToJson: false)
class StartTestDataDto {
/// Authenticated attempt identifier.
@JsonKey(name: 'attempt_id')
final int attemptId;

/// Testing summary.
final TestAttemptTestingDto testing;

/// Current exercise returned by the backend.
@JsonKey(name: 'current_exercise')
final TestingExerciseDto currentExercise;

/// Creates an instance of [StartTestDataDto].
StartTestDataDto({
required this.attemptId,
required this.testing,
required this.currentExercise,
});

/// Creates a [StartTestDataDto] from JSON.
factory StartTestDataDto.fromJson(Map<String, dynamic> json) => _$StartTestDataDtoFromJson(json);
}
19 changes: 19 additions & 0 deletions lib/features/tests/attempt/data/dto/start_test_response_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'package:json_annotation/json_annotation.dart';

import 'start_test_data_dto.dart';

part 'start_test_response_dto.g.dart';

/// DTO envelope for authenticated test start response.
@JsonSerializable(createToJson: false)
class StartTestResponseDto {
/// Authenticated test attempt payload.
final StartTestDataDto data;

/// Creates an instance of [StartTestResponseDto].
StartTestResponseDto({required this.data});

/// Creates a [StartTestResponseDto] from JSON.
factory StartTestResponseDto.fromJson(Map<String, dynamic> json) =>
_$StartTestResponseDtoFromJson(json);
}
17 changes: 14 additions & 3 deletions lib/features/tests/attempt/data/mappers/test_attempt_mapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import '../../domain/entities/test_attempt_result.dart';
import '../../domain/entities/test_attempt_start.dart';
import '../../domain/entities/test_attempt_testing.dart';
import '../../domain/entities/testing_exercise.dart';
import '../dto/save_guest_test_result_data_dto.dart';
import '../dto/save_test_result_data_dto.dart';
import '../dto/start_guest_test_data_dto.dart';
import '../dto/start_test_data_dto.dart';
import '../dto/test_attempt_testing_dto.dart';
import '../dto/testing_exercise_dto.dart';

Expand Down Expand Up @@ -38,8 +39,18 @@ extension StartGuestTestMapper on StartGuestTestDataDto {
);
}

/// Extension that maps [SaveGuestTestResultDataDto] to [TestAttemptResult].
extension SaveGuestTestResultMapper on SaveGuestTestResultDataDto {
/// Extension that maps [StartTestDataDto] to [TestAttemptStart].
extension StartTestMapper on StartTestDataDto {
/// Converts DTO to a domain entity.
TestAttemptStart toEntity() => TestAttemptStart(
attemptId: attemptId.toString(),
testing: testing.toEntity(),
currentExercise: currentExercise.toEntity(),
);
}

/// Extension that maps save-result DTOs to [TestAttemptResult].
extension SaveGuestTestResultMapper on SaveTestResultDataDto {
/// Converts DTO to a domain entity.
TestAttemptResult toEntity() => TestAttemptResult(
nextExercise: nextExercise?.toEntity(),
Expand Down
Loading
Loading