6/8 - Public Beta (Discord)
|See Changelog
Skip to main content

Coaching API

The Coaching API provides comprehensive program customization tools for fitness coaches, enabling them to modify client programs through exercise overrides and additions. This system allows coaches to make precise, targeted adjustments to training programs while maintaining the underlying program structure and progression logic.

Overview

OpenLift's coaching system operates through two primary mechanisms:

  1. Exercise Overrides: Modify existing exercises in a client's program (substitute exercises, adjust parameters, skip exercises)
  2. Exercise Additions: Add new exercises to specific workouts within a client's program

Both systems support sophisticated parameter control, week-specific targeting, and coach-to-client communication, providing coaches with the flexibility needed for personalized training adjustments.

Core Concepts

Exercise Override

An Exercise Override allows coaches to modify any aspect of an existing exercise in a client's program:

interface ProgramUserExerciseOverride {
id: string;
programUserInstanceId: string; // Target client program
targetMasterPweIdFromSnapshot: string; // Original exercise to override
overrideExerciseId?: string; // Substitute exercise (if replacing)

// Parameter modifications
baseOverrideSets?: number; // Modify base sets
baseOverrideRepsMin?: number; // Modify base rep range
baseOverrideRepsMax?: number;
baseOverrideRestSeconds?: number; // Modify rest periods

// Absolute overrides (highest priority)
absoluteTargetSets?: number; // Force specific sets
absoluteTargetRepsMin?: number; // Force specific rep range
absoluteTargetRepsMax?: number;
absoluteTargetRestSeconds?: number;
absoluteTargetWeight?: number; // Force specific weight

// Multiplier overrides (for deloads, intensity control)
weightMultiplierOverride?: number; // e.g., 0.6 for deload week
repsMultiplierOverride?: number; // Scale reps up/down
rpeMultiplierOverride?: number; // Adjust RPE targets

// Control and communication
skipExercise?: boolean; // Skip exercise entirely
notesForUser?: string; // Coach message to client

// Week targeting
effectiveFromWeek?: number; // Start week for override
effectiveUntilWeek?: number; // End week for override
}

Exercise Addition

An Exercise Addition allows coaches to insert completely new exercises into a client's workouts:

interface ProgramUserAddedExercise {
id: string;
programUserInstanceId: string; // Target client program
targetMasterPwtIdFromSnapshot: string; // Target workout to add to
addedExerciseId: string; // Exercise to add
order: number; // Position in workout

// Base parameters
baseSets: number; // Foundation sets
baseRepsMin: number; // Foundation rep range
baseRepsMax: number;
baseRestSeconds: number; // Foundation rest

// Absolute targets (optional)
absoluteTargetSets?: number; // Override with specific values
absoluteTargetRepsMin?: number;
absoluteTargetRepsMax?: number;
absoluteTargetRestSeconds?: number;

// Communication and targeting
notesForUser?: string; // Coach explanation
effectiveFromWeek?: number; // Start week
effectiveUntilWeek?: number; // End week
}

GraphQL Schema

Query Operations

Get Overrides for Program User Instance

query GetOverridesForPui($puiId: String!) {
getOverridesForPui(puiId: $puiId) {
id
programUserInstanceId
targetMasterPweIdFromSnapshot
overrideExerciseId
coachId
order
skipExercise

# Base parameter overrides
baseOverrideSets
baseOverrideRepsMin
baseOverrideRepsMax
baseOverrideRestSeconds

# Absolute target overrides
absoluteTargetSets
absoluteTargetRepsMin
absoluteTargetRepsMax
absoluteTargetRestSeconds
absoluteTargetWeight

# Multiplier overrides
weightMultiplierOverride
repsMultiplierOverride
rpeMultiplierOverride

# Communication and targeting
notesForUser
effectiveFromWeek
effectiveUntilWeek

# Timestamps
createdAt
updatedAt

# Relations
programUserInstance {
id
userId
programPlanId
}
overrideExercise {
id
name
}
coach {
id
firstName
lastName
}
}
}

Get Overrides for Specific Workout

query GetOverridesForPwtSnapshot(
$puiId: String!
$pwtSnapshotId: String!
$weekNumber: Int
) {
getOverridesForPwtSnapshot(
puiId: $puiId
pwtSnapshotId: $pwtSnapshotId
weekNumber: $weekNumber
) {
id
targetMasterPweIdFromSnapshot
overrideExerciseId
skipExercise
baseOverrideSets
baseOverrideRepsMin
baseOverrideRepsMax
absoluteTargetSets
absoluteTargetWeight
weightMultiplierOverride
notesForUser
effectiveFromWeek
effectiveUntilWeek
overrideExercise {
id
name
}
}
}

Get Added Exercises for Program User Instance

query GetAddedExercisesForPui($puiId: String!) {
getAddedExercisesForPui(puiId: $puiId) {
id
programUserInstanceId
targetMasterPwtIdFromSnapshot
addedExerciseId
coachId
order

# Base parameters
baseSets
baseRepsMin
baseRepsMax
baseRestSeconds

# Absolute targets
absoluteTargetSets
absoluteTargetRepsMin
absoluteTargetRepsMax
absoluteTargetRestSeconds

# Communication and targeting
notesForUser
effectiveFromWeek
effectiveUntilWeek

# Timestamps
createdAt
updatedAt

# Relations
programUserInstance {
id
userId
}
addedExercise {
id
name
muscleGroups {
name
}
}
coach {
id
firstName
lastName
}
}
}

Get Added Exercises for Specific Workout

query GetAddedExercisesForPwtSnapshot(
$puiId: String!
$pwtSnapshotId: String!
$weekNumber: Int
) {
getAddedExercisesForPwtSnapshot(
puiId: $puiId
pwtSnapshotId: $pwtSnapshotId
weekNumber: $weekNumber
) {
id
addedExerciseId
order
baseSets
baseRepsMin
baseRepsMax
baseRestSeconds
notesForUser
effectiveFromWeek
effectiveUntilWeek
addedExercise {
id
name
muscleGroups {
name
}
}
}
}

Mutation Operations

Create Exercise Override

mutation CreateOverride($input: CreateOverrideInput!) {
createOverride(input: $input) {
id
programUserInstanceId
targetMasterPweIdFromSnapshot
overrideExerciseId
skipExercise
baseOverrideSets
baseOverrideRepsMin
baseOverrideRepsMax
absoluteTargetSets
absoluteTargetWeight
weightMultiplierOverride
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
overrideExercise {
id
name
}
}
}

Update Exercise Override

mutation UpdateOverride($overrideId: String!, $input: UpdateOverrideInput!) {
updateOverride(overrideId: $overrideId, input: $input) {
id
overrideExerciseId
skipExercise
baseOverrideSets
absoluteTargetWeight
weightMultiplierOverride
notesForUser
effectiveFromWeek
effectiveUntilWeek
updatedAt
}
}

Delete Exercise Override

mutation DeleteOverride($overrideId: String!) {
deleteOverride(overrideId: $overrideId)
}

Create Added Exercise

mutation CreateAddedExercise($input: CreateAddedExerciseInput!) {
createAddedExercise(input: $input) {
id
programUserInstanceId
targetMasterPwtIdFromSnapshot
addedExerciseId
order
baseSets
baseRepsMin
baseRepsMax
baseRestSeconds
absoluteTargetSets
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
addedExercise {
id
name
}
}
}

Update Added Exercise

mutation UpdateAddedExercise($addedExerciseId: String!, $input: UpdateAddedExerciseInput!) {
updateAddedExercise(addedExerciseId: $addedExerciseId, input: $input) {
id
order
baseSets
baseRepsMin
baseRepsMax
absoluteTargetSets
notesForUser
effectiveFromWeek
effectiveUntilWeek
updatedAt
}
}

Delete Added Exercise

mutation DeleteAddedExercise($addedExerciseId: String!) {
deleteAddedExercise(addedExerciseId: $addedExerciseId)
}

Input Types

Create Override Input

input CreateOverrideInput {
programUserInstanceId: String! # Target client program
targetMasterPweIdFromSnapshot: String! # Exercise to override
overrideExerciseId: String # Substitute exercise
order: Int # Reorder exercise
skipExercise: Boolean # Skip entirely

# Base parameter overrides
baseOverrideSets: Int # Modify base sets
baseOverrideRepsMin: Int # Modify base rep range
baseOverrideRepsMax: Int
baseOverrideRestSeconds: Int # Modify rest periods

# Absolute overrides (highest priority)
absoluteTargetSets: Int # Force specific sets
absoluteTargetRepsMin: Int # Force specific reps
absoluteTargetRepsMax: Int
absoluteTargetRestSeconds: Int # Force rest period
absoluteTargetWeight: Float # Force weight

# Multiplier overrides (for deloads, etc.)
weightMultiplierOverride: Float # Scale weight (e.g., 0.6)
repsMultiplierOverride: Float # Scale reps
rpeMultiplierOverride: Float # Scale RPE

# Communication
notesForUser: String # Coach message

# Week targeting
effectiveFromWeek: Int # Start week
effectiveUntilWeek: Int # End week
}

Create Added Exercise Input

input CreateAddedExerciseInput {
programUserInstanceId: String! # Target program
targetMasterPwtIdFromSnapshot: String! # Target workout
addedExerciseId: String! # Exercise to add
order: Int! # Position in workout

# Base parameters (required)
baseSets: Int! # Foundation sets
baseRepsMin: Int! # Foundation rep range
baseRepsMax: Int!
baseRestSeconds: Int! # Foundation rest

# Absolute targets (optional overrides)
absoluteTargetSets: Int # Force specific sets
absoluteTargetRepsMin: Int # Force specific reps
absoluteTargetRepsMax: Int
absoluteTargetRestSeconds: Int # Force rest

# Communication
notesForUser: String # Coach explanation

# Week targeting
effectiveFromWeek: Int # Start week
effectiveUntilWeek: Int # End week
}

Authorization

All coaching operations require coach-level authentication:

// Coach authentication required
final headers = {
'Authorization': 'Bearer $coachAccessToken',
'Content-Type': 'application/json',
};

Flutter Integration

Setting Up GraphQL Operations

1. Define GraphQL Documents

// lib/graphql/coaching_queries.dart
const String GET_OVERRIDES_FOR_PUI = '''
query GetOverridesForPui(\$puiId: String!) {
getOverridesForPui(puiId: \$puiId) {
id
programUserInstanceId
targetMasterPweIdFromSnapshot
overrideExerciseId
skipExercise
baseOverrideSets
baseOverrideRepsMin
baseOverrideRepsMax
baseOverrideRestSeconds
absoluteTargetSets
absoluteTargetRepsMin
absoluteTargetRepsMax
absoluteTargetRestSeconds
absoluteTargetWeight
weightMultiplierOverride
repsMultiplierOverride
rpeMultiplierOverride
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
updatedAt
overrideExercise {
id
name
}
}
}
''';

const String CREATE_OVERRIDE = '''
mutation CreateOverride(\$input: CreateOverrideInput!) {
createOverride(input: \$input) {
id
programUserInstanceId
targetMasterPweIdFromSnapshot
overrideExerciseId
skipExercise
baseOverrideSets
baseOverrideRepsMin
baseOverrideRepsMax
absoluteTargetSets
absoluteTargetWeight
weightMultiplierOverride
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
overrideExercise {
id
name
}
}
}
''';

const String GET_ADDED_EXERCISES_FOR_PUI = '''
query GetAddedExercisesForPui(\$puiId: String!) {
getAddedExercisesForPui(puiId: \$puiId) {
id
programUserInstanceId
targetMasterPwtIdFromSnapshot
addedExerciseId
order
baseSets
baseRepsMin
baseRepsMax
baseRestSeconds
absoluteTargetSets
absoluteTargetRepsMin
absoluteTargetRepsMax
absoluteTargetRestSeconds
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
updatedAt
addedExercise {
id
name
muscleGroups {
name
}
}
}
}
''';

const String CREATE_ADDED_EXERCISE = '''
mutation CreateAddedExercise(\$input: CreateAddedExerciseInput!) {
createAddedExercise(input: \$input) {
id
programUserInstanceId
targetMasterPwtIdFromSnapshot
addedExerciseId
order
baseSets
baseRepsMin
baseRepsMax
baseRestSeconds
absoluteTargetSets
notesForUser
effectiveFromWeek
effectiveUntilWeek
createdAt
addedExercise {
id
name
}
}
}
''';

2. Create Data Models

// lib/models/coaching.dart
class ProgramUserExerciseOverride {
final String id;
final String programUserInstanceId;
final String targetMasterPweIdFromSnapshot;
final String? overrideExerciseId;
final int? order;
final bool? skipExercise;

// Base parameter overrides
final int? baseOverrideSets;
final int? baseOverrideRepsMin;
final int? baseOverrideRepsMax;
final int? baseOverrideRestSeconds;

// Absolute target overrides
final int? absoluteTargetSets;
final int? absoluteTargetRepsMin;
final int? absoluteTargetRepsMax;
final int? absoluteTargetRestSeconds;
final double? absoluteTargetWeight;

// Multiplier overrides
final double? weightMultiplierOverride;
final double? repsMultiplierOverride;
final double? rpeMultiplierOverride;

// Communication and targeting
final String? notesForUser;
final int? effectiveFromWeek;
final int? effectiveUntilWeek;

// Timestamps
final DateTime createdAt;
final DateTime updatedAt;

// Related data
final Exercise? overrideExercise;

ProgramUserExerciseOverride({
required this.id,
required this.programUserInstanceId,
required this.targetMasterPweIdFromSnapshot,
this.overrideExerciseId,
this.order,
this.skipExercise,
this.baseOverrideSets,
this.baseOverrideRepsMin,
this.baseOverrideRepsMax,
this.baseOverrideRestSeconds,
this.absoluteTargetSets,
this.absoluteTargetRepsMin,
this.absoluteTargetRepsMax,
this.absoluteTargetRestSeconds,
this.absoluteTargetWeight,
this.weightMultiplierOverride,
this.repsMultiplierOverride,
this.rpeMultiplierOverride,
this.notesForUser,
this.effectiveFromWeek,
this.effectiveUntilWeek,
required this.createdAt,
required this.updatedAt,
this.overrideExercise,
});

factory ProgramUserExerciseOverride.fromJson(Map<String, dynamic> json) {
return ProgramUserExerciseOverride(
id: json['id'],
programUserInstanceId: json['programUserInstanceId'],
targetMasterPweIdFromSnapshot: json['targetMasterPweIdFromSnapshot'],
overrideExerciseId: json['overrideExerciseId'],
order: json['order'],
skipExercise: json['skipExercise'],
baseOverrideSets: json['baseOverrideSets'],
baseOverrideRepsMin: json['baseOverrideRepsMin'],
baseOverrideRepsMax: json['baseOverrideRepsMax'],
baseOverrideRestSeconds: json['baseOverrideRestSeconds'],
absoluteTargetSets: json['absoluteTargetSets'],
absoluteTargetRepsMin: json['absoluteTargetRepsMin'],
absoluteTargetRepsMax: json['absoluteTargetRepsMax'],
absoluteTargetRestSeconds: json['absoluteTargetRestSeconds'],
absoluteTargetWeight: json['absoluteTargetWeight']?.toDouble(),
weightMultiplierOverride: json['weightMultiplierOverride']?.toDouble(),
repsMultiplierOverride: json['repsMultiplierOverride']?.toDouble(),
rpeMultiplierOverride: json['rpeMultiplierOverride']?.toDouble(),
notesForUser: json['notesForUser'],
effectiveFromWeek: json['effectiveFromWeek'],
effectiveUntilWeek: json['effectiveUntilWeek'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
overrideExercise: json['overrideExercise'] != null
? Exercise.fromJson(json['overrideExercise'])
: null,
);
}
}

class ProgramUserAddedExercise {
final String id;
final String programUserInstanceId;
final String targetMasterPwtIdFromSnapshot;
final String addedExerciseId;
final int order;

// Base parameters
final int baseSets;
final int baseRepsMin;
final int baseRepsMax;
final int baseRestSeconds;

// Absolute targets
final int? absoluteTargetSets;
final int? absoluteTargetRepsMin;
final int? absoluteTargetRepsMax;
final int? absoluteTargetRestSeconds;

// Communication and targeting
final String? notesForUser;
final int? effectiveFromWeek;
final int? effectiveUntilWeek;

// Timestamps
final DateTime createdAt;
final DateTime updatedAt;

// Related data
final Exercise addedExercise;

ProgramUserAddedExercise({
required this.id,
required this.programUserInstanceId,
required this.targetMasterPwtIdFromSnapshot,
required this.addedExerciseId,
required this.order,
required this.baseSets,
required this.baseRepsMin,
required this.baseRepsMax,
required this.baseRestSeconds,
this.absoluteTargetSets,
this.absoluteTargetRepsMin,
this.absoluteTargetRepsMax,
this.absoluteTargetRestSeconds,
this.notesForUser,
this.effectiveFromWeek,
this.effectiveUntilWeek,
required this.createdAt,
required this.updatedAt,
required this.addedExercise,
});

factory ProgramUserAddedExercise.fromJson(Map<String, dynamic> json) {
return ProgramUserAddedExercise(
id: json['id'],
programUserInstanceId: json['programUserInstanceId'],
targetMasterPwtIdFromSnapshot: json['targetMasterPwtIdFromSnapshot'],
addedExerciseId: json['addedExerciseId'],
order: json['order'],
baseSets: json['baseSets'],
baseRepsMin: json['baseRepsMin'],
baseRepsMax: json['baseRepsMax'],
baseRestSeconds: json['baseRestSeconds'],
absoluteTargetSets: json['absoluteTargetSets'],
absoluteTargetRepsMin: json['absoluteTargetRepsMin'],
absoluteTargetRepsMax: json['absoluteTargetRepsMax'],
absoluteTargetRestSeconds: json['absoluteTargetRestSeconds'],
notesForUser: json['notesForUser'],
effectiveFromWeek: json['effectiveFromWeek'],
effectiveUntilWeek: json['effectiveUntilWeek'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
addedExercise: Exercise.fromJson(json['addedExercise']),
);
}
}

3. Create Service Class

// lib/services/coaching_service.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import '../graphql/coaching_queries.dart';
import '../models/coaching.dart';

class CoachingService {
final GraphQLClient _client;

CoachingService(this._client);

/// Get all exercise overrides for a program user instance
Future<List<ProgramUserExerciseOverride>> getOverridesForPui(String puiId) async {
final options = QueryOptions(
document: gql(GET_OVERRIDES_FOR_PUI),
variables: {'puiId': puiId},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.query(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['getOverridesForPui'] as List? ?? [];
return data.map((e) => ProgramUserExerciseOverride.fromJson(e)).toList();
}

/// Get all added exercises for a program user instance
Future<List<ProgramUserAddedExercise>> getAddedExercisesForPui(String puiId) async {
final options = QueryOptions(
document: gql(GET_ADDED_EXERCISES_FOR_PUI),
variables: {'puiId': puiId},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.query(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['getAddedExercisesForPui'] as List? ?? [];
return data.map((e) => ProgramUserAddedExercise.fromJson(e)).toList();
}

/// Create an exercise override
Future<ProgramUserExerciseOverride> createOverride({
required String programUserInstanceId,
required String targetMasterPweIdFromSnapshot,
String? overrideExerciseId,
int? order,
bool? skipExercise,
int? baseOverrideSets,
int? baseOverrideRepsMin,
int? baseOverrideRepsMax,
int? baseOverrideRestSeconds,
int? absoluteTargetSets,
int? absoluteTargetRepsMin,
int? absoluteTargetRepsMax,
int? absoluteTargetRestSeconds,
double? absoluteTargetWeight,
double? weightMultiplierOverride,
double? repsMultiplierOverride,
double? rpeMultiplierOverride,
String? notesForUser,
int? effectiveFromWeek,
int? effectiveUntilWeek,
}) async {
final options = MutationOptions(
document: gql(CREATE_OVERRIDE),
variables: {
'input': {
'programUserInstanceId': programUserInstanceId,
'targetMasterPweIdFromSnapshot': targetMasterPweIdFromSnapshot,
if (overrideExerciseId != null) 'overrideExerciseId': overrideExerciseId,
if (order != null) 'order': order,
if (skipExercise != null) 'skipExercise': skipExercise,
if (baseOverrideSets != null) 'baseOverrideSets': baseOverrideSets,
if (baseOverrideRepsMin != null) 'baseOverrideRepsMin': baseOverrideRepsMin,
if (baseOverrideRepsMax != null) 'baseOverrideRepsMax': baseOverrideRepsMax,
if (baseOverrideRestSeconds != null) 'baseOverrideRestSeconds': baseOverrideRestSeconds,
if (absoluteTargetSets != null) 'absoluteTargetSets': absoluteTargetSets,
if (absoluteTargetRepsMin != null) 'absoluteTargetRepsMin': absoluteTargetRepsMin,
if (absoluteTargetRepsMax != null) 'absoluteTargetRepsMax': absoluteTargetRepsMax,
if (absoluteTargetRestSeconds != null) 'absoluteTargetRestSeconds': absoluteTargetRestSeconds,
if (absoluteTargetWeight != null) 'absoluteTargetWeight': absoluteTargetWeight,
if (weightMultiplierOverride != null) 'weightMultiplierOverride': weightMultiplierOverride,
if (repsMultiplierOverride != null) 'repsMultiplierOverride': repsMultiplierOverride,
if (rpeMultiplierOverride != null) 'rpeMultiplierOverride': rpeMultiplierOverride,
if (notesForUser != null) 'notesForUser': notesForUser,
if (effectiveFromWeek != null) 'effectiveFromWeek': effectiveFromWeek,
if (effectiveUntilWeek != null) 'effectiveUntilWeek': effectiveUntilWeek,
},
},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['createOverride'];
if (data == null) {
throw Exception('No override data received');
}

return ProgramUserExerciseOverride.fromJson(data);
}

/// Create an added exercise
Future<ProgramUserAddedExercise> createAddedExercise({
required String programUserInstanceId,
required String targetMasterPwtIdFromSnapshot,
required String addedExerciseId,
required int order,
required int baseSets,
required int baseRepsMin,
required int baseRepsMax,
required int baseRestSeconds,
int? absoluteTargetSets,
int? absoluteTargetRepsMin,
int? absoluteTargetRepsMax,
int? absoluteTargetRestSeconds,
String? notesForUser,
int? effectiveFromWeek,
int? effectiveUntilWeek,
}) async {
final options = MutationOptions(
document: gql(CREATE_ADDED_EXERCISE),
variables: {
'input': {
'programUserInstanceId': programUserInstanceId,
'targetMasterPwtIdFromSnapshot': targetMasterPwtIdFromSnapshot,
'addedExerciseId': addedExerciseId,
'order': order,
'baseSets': baseSets,
'baseRepsMin': baseRepsMin,
'baseRepsMax': baseRepsMax,
'baseRestSeconds': baseRestSeconds,
if (absoluteTargetSets != null) 'absoluteTargetSets': absoluteTargetSets,
if (absoluteTargetRepsMin != null) 'absoluteTargetRepsMin': absoluteTargetRepsMin,
if (absoluteTargetRepsMax != null) 'absoluteTargetRepsMax': absoluteTargetRepsMax,
if (absoluteTargetRestSeconds != null) 'absoluteTargetRestSeconds': absoluteTargetRestSeconds,
if (notesForUser != null) 'notesForUser': notesForUser,
if (effectiveFromWeek != null) 'effectiveFromWeek': effectiveFromWeek,
if (effectiveUntilWeek != null) 'effectiveUntilWeek': effectiveUntilWeek,
},
},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

final data = result.data?['createAddedExercise'];
if (data == null) {
throw Exception('No added exercise data received');
}

return ProgramUserAddedExercise.fromJson(data);
}

/// Delete an exercise override
Future<bool> deleteOverride(String overrideId) async {
final options = MutationOptions(
document: gql('''
mutation DeleteOverride(\$overrideId: String!) {
deleteOverride(overrideId: \$overrideId)
}
'''),
variables: {'overrideId': overrideId},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

return result.data?['deleteOverride'] == true;
}

/// Delete an added exercise
Future<bool> deleteAddedExercise(String addedExerciseId) async {
final options = MutationOptions(
document: gql('''
mutation DeleteAddedExercise(\$addedExerciseId: String!) {
deleteAddedExercise(addedExerciseId: \$addedExerciseId)
}
'''),
variables: {'addedExerciseId': addedExerciseId},
errorPolicy: ErrorPolicy.all,
);

final result = await _client.mutate(options);

if (result.hasException) {
throw result.exception!;
}

return result.data?['deleteAddedExercise'] == true;
}
}

4. State Management with Provider

// lib/providers/coaching_provider.dart
import 'package:flutter/foundation.dart';
import '../services/coaching_service.dart';
import '../models/coaching.dart';

class CoachingProvider extends ChangeNotifier {
final CoachingService _service;

List<ProgramUserExerciseOverride> _overrides = [];
List<ProgramUserAddedExercise> _addedExercises = [];
bool _isLoading = false;
String? _errorMessage;

CoachingProvider(this._service);

// Getters
List<ProgramUserExerciseOverride> get overrides => _overrides;
List<ProgramUserAddedExercise> get addedExercises => _addedExercises;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;

/// Load all coaching modifications for a client's program
Future<void> loadClientModifications(String puiId) async {
_isLoading = true;
_errorMessage = null;
notifyListeners();

try {
final results = await Future.wait([
_service.getOverridesForPui(puiId),
_service.getAddedExercisesForPui(puiId),
]);

_overrides = results[0] as List<ProgramUserExerciseOverride>;
_addedExercises = results[1] as List<ProgramUserAddedExercise>;
} catch (e) {
_errorMessage = e.toString();
} finally {
_isLoading = false;
notifyListeners();
}
}

/// Create an exercise override
Future<void> createOverride({
required String programUserInstanceId,
required String targetMasterPweIdFromSnapshot,
String? overrideExerciseId,
int? order,
bool? skipExercise,
int? baseOverrideSets,
int? baseOverrideRepsMin,
int? baseOverrideRepsMax,
double? weightMultiplierOverride,
String? notesForUser,
int? effectiveFromWeek,
int? effectiveUntilWeek,
}) async {
try {
final override = await _service.createOverride(
programUserInstanceId: programUserInstanceId,
targetMasterPweIdFromSnapshot: targetMasterPweIdFromSnapshot,
overrideExerciseId: overrideExerciseId,
order: order,
skipExercise: skipExercise,
baseOverrideSets: baseOverrideSets,
baseOverrideRepsMin: baseOverrideRepsMin,
baseOverrideRepsMax: baseOverrideRepsMax,
weightMultiplierOverride: weightMultiplierOverride,
notesForUser: notesForUser,
effectiveFromWeek: effectiveFromWeek,
effectiveUntilWeek: effectiveUntilWeek,
);

_overrides.add(override);
_overrides.sort((a, b) => b.createdAt.compareTo(a.createdAt));
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
rethrow;
}
}

/// Create an added exercise
Future<void> createAddedExercise({
required String programUserInstanceId,
required String targetMasterPwtIdFromSnapshot,
required String addedExerciseId,
required int order,
required int baseSets,
required int baseRepsMin,
required int baseRepsMax,
required int baseRestSeconds,
String? notesForUser,
int? effectiveFromWeek,
int? effectiveUntilWeek,
}) async {
try {
final addedExercise = await _service.createAddedExercise(
programUserInstanceId: programUserInstanceId,
targetMasterPwtIdFromSnapshot: targetMasterPwtIdFromSnapshot,
addedExerciseId: addedExerciseId,
order: order,
baseSets: baseSets,
baseRepsMin: baseRepsMin,
baseRepsMax: baseRepsMax,
baseRestSeconds: baseRestSeconds,
notesForUser: notesForUser,
effectiveFromWeek: effectiveFromWeek,
effectiveUntilWeek: effectiveUntilWeek,
);

_addedExercises.add(addedExercise);
_addedExercises.sort((a, b) => b.createdAt.compareTo(a.createdAt));
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
rethrow;
}
}

/// Remove an exercise override
Future<void> deleteOverride(String overrideId) async {
try {
final success = await _service.deleteOverride(overrideId);
if (success) {
_overrides.removeWhere((override) => override.id == overrideId);
notifyListeners();
}
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
rethrow;
}
}

/// Remove an added exercise
Future<void> deleteAddedExercise(String addedExerciseId) async {
try {
final success = await _service.deleteAddedExercise(addedExerciseId);
if (success) {
_addedExercises.removeWhere((exercise) => exercise.id == addedExerciseId);
notifyListeners();
}
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
rethrow;
}
}
}

5. UI Implementation

// lib/screens/coach_client_program_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/coaching_provider.dart';
import '../models/coaching.dart';

class CoachClientProgramScreen extends StatefulWidget {
final String clientPuiId;
final String clientName;

const CoachClientProgramScreen({
Key? key,
required this.clientPuiId,
required this.clientName,
}) : super(key: key);

@override
_CoachClientProgramScreenState createState() => _CoachClientProgramScreenState();
}

class _CoachClientProgramScreenState extends State<CoachClientProgramScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadClientModifications();
});
}

Future<void> _loadClientModifications() async {
final provider = context.read<CoachingProvider>();
await provider.loadClientModifications(widget.clientPuiId);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${widget.clientName} Program'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: _loadClientModifications,
),
],
),
body: Consumer<CoachingProvider>(
builder: (context, provider, child) {
if (provider.isLoading && provider.overrides.isEmpty && provider.addedExercises.isEmpty) {
return Center(child: CircularProgressIndicator());
}

if (provider.errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
SizedBox(height: 16),
Text('Error loading program modifications'),
SizedBox(height: 8),
Text(provider.errorMessage!),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadClientModifications,
child: Text('Retry'),
),
],
),
);
}

return RefreshIndicator(
onRefresh: _loadClientModifications,
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildQuickActions(),
SizedBox(height: 16),
_buildOverridesSection(provider.overrides),
SizedBox(height: 16),
_buildAddedExercisesSection(provider.addedExercises),
],
),
),
);
},
),
);
}

Widget _buildQuickActions() {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Program Modifications',
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _showCreateOverrideDialog(),
icon: Icon(Icons.edit_outlined),
label: Text('Override Exercise'),
),
),
SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _showAddExerciseDialog(),
icon: Icon(Icons.add_circle_outline),
label: Text('Add Exercise'),
),
),
],
),
],
),
),
);
}

Widget _buildOverridesSection(List<ProgramUserExerciseOverride> overrides) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Exercise Overrides',
style: Theme.of(context).textTheme.titleMedium,
),
Chip(
label: Text('${overrides.length}'),
backgroundColor: Colors.blue.shade100,
),
],
),
SizedBox(height: 12),
if (overrides.isEmpty)
_buildEmptyState(
Icons.edit_off_outlined,
'No exercise overrides',
'Override exercises to customize parameters',
)
else
...overrides.map((override) => _buildOverrideCard(override)),
],
),
),
);
}

Widget _buildAddedExercisesSection(List<ProgramUserAddedExercise> addedExercises) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Added Exercises',
style: Theme.of(context).textTheme.titleMedium,
),
Chip(
label: Text('${addedExercises.length}'),
backgroundColor: Colors.green.shade100,
),
],
),
SizedBox(height: 12),
if (addedExercises.isEmpty)
_buildEmptyState(
Icons.add_circle_outlined,
'No added exercises',
'Add exercises to customize workouts',
)
else
...addedExercises.map((exercise) => _buildAddedExerciseCard(exercise)),
],
),
),
);
}

Widget _buildOverrideCard(ProgramUserExerciseOverride override) {
return Card(
margin: EdgeInsets.only(bottom: 8),
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
override.overrideExercise?.name ?? 'Exercise Override',
style: TextStyle(fontWeight: FontWeight.w500),
),
if (override.skipExercise == true) ...[
SizedBox(height: 4),
Chip(
label: Text('SKIPPED'),
backgroundColor: Colors.red.shade100,
labelStyle: TextStyle(color: Colors.red.shade700),
),
],
],
),
),
IconButton(
onPressed: () => _confirmDeleteOverride(override),
icon: Icon(Icons.delete_outline),
iconSize: 20,
),
],
),
if (override.notesForUser != null) ...[
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(Icons.note_outlined, size: 16, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
override.notesForUser!,
style: TextStyle(color: Colors.blue.shade700),
),
),
],
),
),
],
if (_hasParameterOverrides(override)) ...[
SizedBox(height: 8),
_buildParameterOverrides(override),
],
if (override.effectiveFromWeek != null || override.effectiveUntilWeek != null) ...[
SizedBox(height: 8),
_buildWeekTargeting(override.effectiveFromWeek, override.effectiveUntilWeek),
],
],
),
),
);
}

Widget _buildAddedExerciseCard(ProgramUserAddedExercise exercise) {
return Card(
margin: EdgeInsets.only(bottom: 8),
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
exercise.addedExercise.name,
style: TextStyle(fontWeight: FontWeight.w500),
),
Text(
'Order: ${exercise.order}',
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
IconButton(
onPressed: () => _confirmDeleteAddedExercise(exercise),
icon: Icon(Icons.delete_outline),
iconSize: 20,
),
],
),
SizedBox(height: 8),
_buildExerciseParameters(
sets: exercise.absoluteTargetSets ?? exercise.baseSets,
repsMin: exercise.absoluteTargetRepsMin ?? exercise.baseRepsMin,
repsMax: exercise.absoluteTargetRepsMax ?? exercise.baseRepsMax,
restSeconds: exercise.absoluteTargetRestSeconds ?? exercise.baseRestSeconds,
),
if (exercise.notesForUser != null) ...[
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Icon(Icons.note_outlined, size: 16, color: Colors.green),
SizedBox(width: 8),
Expanded(
child: Text(
exercise.notesForUser!,
style: TextStyle(color: Colors.green.shade700),
),
),
],
),
),
],
if (exercise.effectiveFromWeek != null || exercise.effectiveUntilWeek != null) ...[
SizedBox(height: 8),
_buildWeekTargeting(exercise.effectiveFromWeek, exercise.effectiveUntilWeek),
],
],
),
),
);
}

Widget _buildEmptyState(IconData icon, String title, String subtitle) {
return Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Column(
children: [
Icon(icon, size: 48, color: Colors.grey),
SizedBox(height: 16),
Text(
title,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
),
SizedBox(height: 8),
Text(
subtitle,
style: TextStyle(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
],
),
),
);
}

bool _hasParameterOverrides(ProgramUserExerciseOverride override) {
return override.baseOverrideSets != null ||
override.baseOverrideRepsMin != null ||
override.baseOverrideRepsMax != null ||
override.absoluteTargetSets != null ||
override.absoluteTargetWeight != null ||
override.weightMultiplierOverride != null;
}

Widget _buildParameterOverrides(ProgramUserExerciseOverride override) {
final overrides = <String>[];

if (override.baseOverrideSets != null) {
overrides.add('Sets: ${override.baseOverrideSets}');
}
if (override.baseOverrideRepsMin != null || override.baseOverrideRepsMax != null) {
final min = override.baseOverrideRepsMin;
final max = override.baseOverrideRepsMax;
if (min != null && max != null && min == max) {
overrides.add('Reps: $min');
} else {
overrides.add('Reps: ${min ?? '?'}-${max ?? '?'}');
}
}
if (override.absoluteTargetWeight != null) {
overrides.add('Weight: ${override.absoluteTargetWeight} lbs');
}
if (override.weightMultiplierOverride != null) {
overrides.add('Weight Multiplier: ${override.weightMultiplierOverride}x');
}

return Wrap(
spacing: 8,
runSpacing: 4,
children: overrides.map((override) => Chip(
label: Text(override),
backgroundColor: Colors.orange.shade100,
labelStyle: TextStyle(fontSize: 12),
)).toList(),
);
}

Widget _buildExerciseParameters({
required int sets,
required int repsMin,
required int repsMax,
required int restSeconds,
}) {
return Row(
children: [
_buildParameterChip('${sets} sets'),
SizedBox(width: 8),
_buildParameterChip('${repsMin}-${repsMax} reps'),
SizedBox(width: 8),
_buildParameterChip('${Duration(seconds: restSeconds).inMinutes}:${(restSeconds % 60).toString().padLeft(2, '0')} rest'),
],
);
}

Widget _buildParameterChip(String text) {
return Chip(
label: Text(text),
backgroundColor: Colors.grey.shade200,
labelStyle: TextStyle(fontSize: 12),
);
}

Widget _buildWeekTargeting(int? fromWeek, int? untilWeek) {
String text;
if (fromWeek != null && untilWeek != null) {
text = 'Weeks $fromWeek-$untilWeek';
} else if (fromWeek != null) {
text = 'From week $fromWeek';
} else if (untilWeek != null) {
text = 'Until week $untilWeek';
} else {
return SizedBox.shrink();
}

return Row(
children: [
Icon(Icons.calendar_today_outlined, size: 16, color: Colors.grey),
SizedBox(width: 4),
Text(
text,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
);
}

void _showCreateOverrideDialog() {
showDialog(
context: context,
builder: (context) => CreateOverrideDialog(
puiId: widget.clientPuiId,
onCreated: () => _loadClientModifications(),
),
);
}

void _showAddExerciseDialog() {
showDialog(
context: context,
builder: (context) => AddExerciseDialog(
puiId: widget.clientPuiId,
onCreated: () => _loadClientModifications(),
),
);
}

void _confirmDeleteOverride(ProgramUserExerciseOverride override) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete Override?'),
content: Text('This will remove the exercise override and restore the original exercise parameters.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
try {
await context.read<CoachingProvider>().deleteOverride(override.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Override deleted successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete override')),
);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Delete'),
),
],
),
);
}

void _confirmDeleteAddedExercise(ProgramUserAddedExercise exercise) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Remove Exercise?'),
content: Text('This will remove "${exercise.addedExercise.name}" from the client\'s program.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
Navigator.pop(context);
try {
await context.read<CoachingProvider>().deleteAddedExercise(exercise.id);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Exercise removed successfully')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to remove exercise')),
);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Remove'),
),
],
),
);
}
}

Common Use Cases

1. Deload Week Implementation

// Create a deload override with weight multiplier
await coachingProvider.createOverride(
programUserInstanceId: client.puiId,
targetMasterPweIdFromSnapshot: exerciseSnapshot.id,
weightMultiplierOverride: 0.6, // 60% of normal weight
repsMultiplierOverride: 0.8, // 80% of normal reps
notesForUser: 'Deload week - focus on form and recovery',
effectiveFromWeek: 4,
effectiveUntilWeek: 4,
);

2. Exercise Substitution

// Replace an exercise with a different one
await coachingProvider.createOverride(
programUserInstanceId: client.puiId,
targetMasterPweIdFromSnapshot: originalExercise.id,
overrideExerciseId: substituteExercise.id,
notesForUser: 'Substituted due to shoulder discomfort',
);

3. Add Accessory Exercise

// Add a new exercise to the end of a workout
await coachingProvider.createAddedExercise(
programUserInstanceId: client.puiId,
targetMasterPwtIdFromSnapshot: workout.id,
addedExerciseId: accessoryExercise.id,
order: 99, // Add at the end
baseSets: 3,
baseRepsMin: 12,
baseRepsMax: 15,
baseRestSeconds: 60,
notesForUser: 'Added for additional volume',
);

4. Temporary Parameter Adjustment

// Override sets and reps for a specific week
await coachingProvider.createOverride(
programUserInstanceId: client.puiId,
targetMasterPweIdFromSnapshot: exercise.id,
absoluteTargetSets: 5, // Force 5 sets instead of program default
absoluteTargetRepsMin: 3, // Force 3-5 reps instead of program default
absoluteTargetRepsMax: 5,
effectiveFromWeek: 6, // Only for week 6
effectiveUntilWeek: 6,
notesForUser: 'Testing 1RM strength this week',
);

Error Handling

Authorization Errors

// Handle coach-only operations
try {
await coachingService.createOverride(...);
} catch (e) {
if (e.toString().contains('isCoach')) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Access Denied'),
content: Text('Only coaches can modify client programs.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
),
);
}
}

Validation Errors

// Handle invalid parameter combinations
try {
await coachingService.createOverride(
baseOverrideSets: -1, // Invalid
// ...
);
} catch (e) {
if (e.toString().contains('BAD_USER_INPUT')) {
showSnackBar('Invalid parameters: Sets must be positive');
}
}

Best Practices

1. Parameter Hierarchy

  • Absolute targets take highest priority
  • Base overrides modify program foundations
  • Multipliers scale existing values
  • Use appropriate level for the intended modification

2. Week Targeting

  • Use specific weeks for temporary changes
  • Leave null for permanent modifications
  • Consider program phase when setting weeks

3. Client Communication

  • Always include notesForUser for context
  • Explain the reasoning behind modifications
  • Use clear, actionable language

4. Exercise Management

  • Prefer overrides to maintain program structure
  • Use additions sparingly to avoid program bloat
  • Consider exercise order when adding new movements

5. Coach Workflow

  • Review modifications regularly
  • Remove outdated overrides
  • Document reasoning for complex changes

The Coaching API provides powerful tools for program customization while maintaining the integrity of the underlying training system. Through careful use of overrides and additions, coaches can deliver highly personalized training experiences that adapt to individual client needs and circumstances.