Files
Core_Be/Services/WorkoutService.cs
T
Dhananjaya99 bffc9b1686 first commit
2026-09-28 11:44:26 +05:30

1181 lines
50 KiB
C#

using Fitness_core_BE.Models;
using Fitness_core_BE.Helpers;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Exceptions;
namespace Fitness_core_BE.Services.Workout
{
public class WorkoutService : IWorkoutService
{
private readonly IWorkoutRepository _repository;
private readonly UserRoleHelper _userRoleHelper;
private readonly IUnitOfWork _uow;
private readonly IActivityLogService _activityLogService;
private readonly IPushNotificationService _pushNotificationService;
public WorkoutService(IWorkoutRepository repository, UserRoleHelper userRoleHelper, IUnitOfWork uow, IActivityLogService activityLogService, IPushNotificationService pushNotificationService)
{
_repository = repository;
_userRoleHelper = userRoleHelper;
_uow = uow;
_activityLogService = activityLogService;
_pushNotificationService = pushNotificationService;
}
private static string? GetOptionalJsonText(System.Text.Json.JsonElement source, params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
if (!source.TryGetProperty(propertyName, out var value) ||
value.ValueKind is System.Text.Json.JsonValueKind.Null or System.Text.Json.JsonValueKind.Undefined)
continue;
return value.ValueKind == System.Text.Json.JsonValueKind.String
? value.GetString()
: value.GetRawText();
}
return null;
}
private static string? GetOptionalJsonText(
Dictionary<string, System.Text.Json.JsonElement> source,
params string[] propertyNames)
{
foreach (var propertyName in propertyNames)
{
if (!source.TryGetValue(propertyName, out var value) ||
value.ValueKind is System.Text.Json.JsonValueKind.Null or System.Text.Json.JsonValueKind.Undefined)
continue;
return value.ValueKind == System.Text.Json.JsonValueKind.String
? value.GetString()
: value.GetRawText();
}
return null;
}
// Parses a day's "exercises" array (structured objects with name/muscleGroup/sets/reps/
// description/breathIn/breathOut/videoUrl) into real WorkoutExercise rows, mirroring
// AdminSuggestionService's persistence for admin-authored suggestions. Legacy plain-string
// exercise entries (no "name" property) are skipped here but still preserved in the
// WorkoutItem.Exercises JSON blob for backward compatibility.
private static List<WorkoutExercise> ParseWorkoutExercisesArray(System.Text.Json.JsonElement exercisesElement)
{
var result = new List<WorkoutExercise>();
if (exercisesElement.ValueKind != System.Text.Json.JsonValueKind.Array) return result;
int order = 1;
foreach (var ex in exercisesElement.EnumerateArray())
{
if (ex.ValueKind != System.Text.Json.JsonValueKind.Object) continue;
if (!ex.TryGetProperty("name", out var nameEl) || nameEl.ValueKind != System.Text.Json.JsonValueKind.String) continue;
var name = nameEl.GetString();
if (string.IsNullOrWhiteSpace(name)) continue;
int? sets = ex.TryGetProperty("sets", out var s) && s.ValueKind == System.Text.Json.JsonValueKind.Number ? s.GetInt32() : (int?)null;
result.Add(new WorkoutExercise
{
WorkoutExerciseId = Guid.NewGuid(),
Name = name.Trim(),
MuscleGroup = GetOptionalJsonText(ex, "muscleGroup"),
Sets = sets,
Reps = GetOptionalJsonText(ex, "reps"),
Description = GetOptionalJsonText(ex, "description"),
BreathIn = GetOptionalJsonText(ex, "breathIn"),
BreathOut = GetOptionalJsonText(ex, "breathOut"),
VideoUrl = GetOptionalJsonText(ex, "videoUrl", "videoURL", "doURL"),
SortOrder = order++,
IsActive = true,
CreateDate = DateTime.UtcNow
});
}
return result;
}
private static List<WorkoutExercise> ParseWorkoutExercises(System.Text.Json.JsonElement itemElement) =>
itemElement.TryGetProperty("exercises", out var exercisesElement) ? ParseWorkoutExercisesArray(exercisesElement) : new List<WorkoutExercise>();
private static List<WorkoutExercise> ParseWorkoutExercises(Dictionary<string, System.Text.Json.JsonElement> source) =>
source.TryGetValue("exercises", out var exercisesElement) ? ParseWorkoutExercisesArray(exercisesElement) : new List<WorkoutExercise>();
// Workout Category operations
public async Task<object> GetWorkoutCategoryList(object payload, HttpContext httpContext)
{
try
{
var categories = await _repository.GetWorkoutCategoryListAsync();
return categories;
}
catch
{
throw;
}
}
public async Task<object> CreateWorkoutCategory(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var workoutCategory = new WorkoutCategory
{
WorkoutCategoryName = data["name"].GetString()!,
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
CreateDate = DateTime.UtcNow
};
await _repository.CreateWorkoutCategoryAsync(workoutCategory);
await _uow.CommitAsync();
// Log activity - non-blocking, failures won't crash the operation
try
{
await _activityLogService.LogActivity(new { description = $"Workout category created with ID {workoutCategory.WorkoutCategoryId}" }, httpContext);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return workoutCategory;
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// Workout Plan operations
public async Task<object> GetWorkoutPlanList(object payload, HttpContext httpContext)
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
string userType = await _userRoleHelper.GetUserType(userId);
Guid clientId;
if (userType == "Customer")
{
Guid cusId = await _userRoleHelper.GetCusId(userId);
try
{
clientId = await _userRoleHelper.GetClientIdByCusId(cusId);
}
catch (NotFoundException)
{
// Not every customer has joined a gym yet — they should still see the
// platform-wide admin suggestions (FKClientId == null in the repo query).
clientId = Guid.Empty;
}
}
else
{
clientId = await _userRoleHelper.GetClientId(userId);
}
try
{
var workoutPlans = await _repository.GetWorkoutPlanListAsync(userType, clientId);
return workoutPlans;
}
catch
{
throw;
}
}
public async Task<object> GetWorkoutPlanById(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
var workoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlanId);
if (workoutPlan == null)
throw new NotFoundException($"Workout plan with ID {workoutPlanId} not found");
// Transform workout items to have exercises as objects instead of JSON strings
var transformedWorkoutItems = workoutPlan.WorkoutItems?.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises; // Keep as string if parsing fails
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.WorkoutExercises,
item.TotalExercises,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList();
return new
{
workoutPlan.WorkoutPlanId,
workoutPlan.Name,
workoutPlan.Description,
workoutPlan.IsTemplate,
workoutPlan.WorkoutCategoryId,
workoutPlan.WorkoutCategoryName,
workoutPlan.ClientId,
workoutPlan.ClientName,
workoutPlan.IsActive,
workoutPlan.CreateDate,
WorkoutItems = transformedWorkoutItems,
AssignedCustomers = workoutPlan.AssignedCustomers
};
}
catch
{
throw;
}
}
public async Task<object> GetWorkoutPlanByCustomerId(object payload, HttpContext httpContext)
{
try
{
Guid customerId;
var data = DeserializePayload.Deserialize(payload);
if (data.ContainsKey("customerId") &&
!string.IsNullOrEmpty(data["customerId"].GetString()))
{
customerId = Guid.Parse(data["customerId"].GetString()!);
}
else
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
customerId = await _userRoleHelper.GetCusId(userId);
}
var workoutPlans = await _repository.GetWorkoutPlansByCustomerIdAsync(customerId);
var transformedPlans = workoutPlans.Select(plan => new
{
plan.WorkoutPlanId,
plan.Name,
plan.Description,
plan.IsTemplate,
plan.WorkoutCategoryId,
plan.WorkoutCategoryName,
plan.ClientId,
plan.ClientName,
plan.IsActive,
plan.CreateDate,
WorkoutItems = plan.WorkoutItems?.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises;
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList(),
AssignedCustomers = plan.AssignedCustomers
}).ToList();
return transformedPlans;
}
catch
{
throw;
}
}
public async Task<object> CreateWorkoutPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
Guid clientId = await _userRoleHelper.GetClientId(userId);
var workoutCategoryId = Guid.Parse(data["workoutCategoryId"].GetString()!);
if (!await _repository.WorkoutCategoryExistsAsync(workoutCategoryId))
throw new NotFoundException("Workout category not found.");
var workoutPlan = new WorkoutPlan
{
WorkoutPlanName = data["name"].GetString()!,
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(),
FKWorkoutCategoryId = workoutCategoryId,
FKClientId = clientId,
CreateDate = DateTime.UtcNow
};
await _repository.CreateWorkoutPlanAsync(workoutPlan);
if (data.ContainsKey("customerIds") && data["customerIds"].ValueKind == System.Text.Json.JsonValueKind.Array)
{
var customerIdsArray = data["customerIds"].EnumerateArray();
foreach (var customerIdElement in customerIdsArray)
{
if (Guid.TryParse(customerIdElement.GetString(), out var customerId))
{
var customerWorkout = new CustomerWorkout
{
FKCustomerId = customerId,
FKWorkoutPlanId = workoutPlan.WorkoutPlanId,
AssignedDate = DateTime.UtcNow,
IsActive = true
};
await _repository.AssignWorkoutToCustomerAsync(customerWorkout);
}
}
}
else if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString()))
{
var customerId = Guid.Parse(data["customerId"].GetString()!);
var customerWorkout = new CustomerWorkout
{
FKCustomerId = customerId,
FKWorkoutPlanId = workoutPlan.WorkoutPlanId,
AssignedDate = DateTime.UtcNow,
IsActive = true
};
await _repository.AssignWorkoutToCustomerAsync(customerWorkout);
}
if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
{
var workoutItemsArray = data["workoutItems"].EnumerateArray();
foreach (var item in workoutItemsArray)
{
// Handle exercises JSON
string? exercisesJson = null;
if (item.TryGetProperty("exercises", out var exercisesElement))
{
if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array ||
exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
exercisesJson = exercisesElement.GetRawText();
}
else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String)
{
exercisesJson = exercisesElement.GetString();
}
}
var workoutItem = new WorkoutItem
{
ExerciseDay = item.GetProperty("exerciseDay").GetString()!,
Description = item.TryGetProperty("description", out var descProp) ? descProp.GetString() : null,
Exercises = exercisesJson,
HowToDo = GetOptionalJsonText(item, "howToDo"),
DoURL = GetOptionalJsonText(item, "doURL", "videoUrl", "videoURL"),
FKWorkoutPlanId = workoutPlan.WorkoutPlanId,
CreateDate = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Duration = GetOptionalJsonText(item, "duration"),
WormUp = GetOptionalJsonText(item, "wormUp", "warmUp", "warmup"),
Cooldown = GetOptionalJsonText(item, "cooldown", "coolDown"),
};
workoutItem.WorkoutExercises = ParseWorkoutExercises(item);
await _repository.CreateWorkoutItemAsync(workoutItem);
}
}
await _uow.CommitAsync();
// Get the created plan with items
var createdWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlan.WorkoutPlanId);
if (createdWorkoutPlan == null)
throw new InvalidOperationException("Failed to retrieve created workout plan");
// Transform workout items to have exercises as objects instead of JSON strings
var transformedWorkoutItems = createdWorkoutPlan.WorkoutItems?.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises; // Keep as string if parsing fails
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.WorkoutExercises,
item.TotalExercises,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList();
try
{
await _activityLogService.LogActivity(new { description = $"Workout plan created with ID {workoutPlan.WorkoutPlanId}" }, httpContext);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return new
{
createdWorkoutPlan.WorkoutPlanId,
createdWorkoutPlan.Name,
createdWorkoutPlan.Description,
createdWorkoutPlan.IsTemplate,
createdWorkoutPlan.WorkoutCategoryId,
createdWorkoutPlan.WorkoutCategoryName,
createdWorkoutPlan.ClientId,
createdWorkoutPlan.ClientName,
createdWorkoutPlan.IsActive,
createdWorkoutPlan.CreateDate,
WorkoutItems = transformedWorkoutItems,
AssignedCustomers = createdWorkoutPlan.AssignedCustomers
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// Same rationale as MealService's equivalent: WorkoutCategory has no FKClientId (it's a
// global taxonomy), so a single well-known "AI Generated" category is get-or-created
// and reused for every gym's AI-generated workout plans, instead of the mobile app
// guessing a category by fetching the list and taking the first entry.
private const string AiGeneratedCategoryName = "AI Generated";
private async Task<Guid> GetOrCreateAiWorkoutCategoryIdAsync()
{
var categories = await _repository.GetWorkoutCategoryListAsync();
var existing = categories.FirstOrDefault(c =>
string.Equals(c.Name, AiGeneratedCategoryName, StringComparison.OrdinalIgnoreCase));
if (existing != null)
return existing.WorkoutCategoryId;
var created = await _repository.CreateWorkoutCategoryAsync(new WorkoutCategory
{
WorkoutCategoryId = Guid.NewGuid(),
WorkoutCategoryName = AiGeneratedCategoryName,
Description = "Plans generated by the AI workout planner",
IsActive = true,
CreateDate = DateTime.UtcNow
});
return created.WorkoutCategoryId;
}
public async Task<object> CreateCustomerWorkout(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid customerId;
if (data.ContainsKey("customerId") &&
!string.IsNullOrEmpty(data["customerId"].GetString()))
{
customerId = Guid.Parse(data["customerId"].GetString()!);
}
else
{
Guid userId = DataExtractor.GetUserIdFromContext(httpContext);
customerId = await _userRoleHelper.GetCusId(userId);
}
// The plan belongs to the customer's own gym, not a fixed placeholder client.
Guid clientId = await _userRoleHelper.GetClientIdByCusId(customerId);
Guid workoutCategoryId = data.ContainsKey("workoutCategoryId") && !string.IsNullOrEmpty(data["workoutCategoryId"].GetString())
? Guid.Parse(data["workoutCategoryId"].GetString()!)
: await GetOrCreateAiWorkoutCategoryIdAsync();
var workoutPlan = new WorkoutPlan
{
WorkoutPlanName = data["name"].GetString()!,
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(),
FKWorkoutCategoryId = workoutCategoryId,
FKClientId = clientId,
CreateDate = DateTime.UtcNow
};
await _repository.CreateWorkoutPlanAsync(workoutPlan);
if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
{
var workoutItemsArray = data["workoutItems"].EnumerateArray();
foreach (var item in workoutItemsArray)
{
string? exercisesJson = null;
if (item.TryGetProperty("exercises", out var exercisesElement))
{
if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array ||
exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
exercisesJson = exercisesElement.GetRawText();
}
else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String)
{
exercisesJson = exercisesElement.GetString();
}
}
var workoutItem = new WorkoutItem
{
ExerciseDay = item.GetProperty("exerciseDay").GetString()!,
Description = item.TryGetProperty("description", out var descProp) ? descProp.GetString() : null,
Exercises = exercisesJson,
HowToDo = GetOptionalJsonText(item, "howToDo"),
DoURL = GetOptionalJsonText(item, "doURL", "videoUrl", "videoURL"),
FKWorkoutPlanId = workoutPlan.WorkoutPlanId,
CreateDate = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Duration = GetOptionalJsonText(item, "duration"),
WormUp = GetOptionalJsonText(item, "wormUp", "warmUp", "warmup"),
Cooldown = GetOptionalJsonText(item, "cooldown", "coolDown"),
};
await _repository.CreateWorkoutItemAsync(workoutItem);
}
}
var customerWorkout = new CustomerWorkout
{
FKCustomerId = customerId,
FKWorkoutPlanId = workoutPlan.WorkoutPlanId,
AssignedDate = DateTime.UtcNow,
IsActive = true,
IsFollowing = true
};
await _repository.AssignWorkoutToCustomerAsync(customerWorkout);
await _uow.CommitAsync();
var createdWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlan.WorkoutPlanId);
if (createdWorkoutPlan == null)
throw new InvalidOperationException("Failed to retrieve created workout plan");
var transformedWorkoutItems = createdWorkoutPlan.WorkoutItems?.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises;
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.WorkoutExercises,
item.TotalExercises,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList();
try
{
await _activityLogService.LogActivity(new { description = $"Customer workout plan created with ID {workoutPlan.WorkoutPlanId}" }, httpContext);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return new
{
createdWorkoutPlan.WorkoutPlanId,
createdWorkoutPlan.Name,
createdWorkoutPlan.Description,
createdWorkoutPlan.IsTemplate,
createdWorkoutPlan.WorkoutCategoryId,
createdWorkoutPlan.WorkoutCategoryName,
createdWorkoutPlan.ClientId,
createdWorkoutPlan.ClientName,
createdWorkoutPlan.IsActive,
createdWorkoutPlan.CreateDate,
WorkoutItems = transformedWorkoutItems,
AssignedCustomers = createdWorkoutPlan.AssignedCustomers
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateWorkoutPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
var existingWorkoutPlan = await _repository.GetWorkoutPlanEntityByIdAsync(workoutPlanId);
if (existingWorkoutPlan == null)
throw new KeyNotFoundException($"Workout plan with ID {workoutPlanId} not found");
// Update workout plan properties
if (data.ContainsKey("name") && !string.IsNullOrEmpty(data["name"].GetString()))
existingWorkoutPlan.WorkoutPlanName = data["name"].GetString()!;
if (data.ContainsKey("description"))
existingWorkoutPlan.Description = data["description"].GetString();
if (data.ContainsKey("isTemplate"))
existingWorkoutPlan.IsTemplate = data["isTemplate"].GetBoolean();
if (data.ContainsKey("workoutCategoryId"))
existingWorkoutPlan.FKWorkoutCategoryId = Guid.Parse(data["workoutCategoryId"].GetString()!);
await _repository.UpdateWorkoutPlanAsync(existingWorkoutPlan);
// Handle workout items update if provided
if (data.ContainsKey("workoutItems") && data["workoutItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
{
// Get existing workout items for this plan
var existingWorkoutItems = await _repository.GetWorkoutItemsByWorkoutPlanIdAsync(workoutPlanId);
// Delete all existing workout items (clean slate approach)
foreach (var existingItem in existingWorkoutItems)
{
await _repository.DeleteWorkoutItemAsync(existingItem.WorkoutItemId);
}
// Create new workout items from payload
var workoutItemsArray = data["workoutItems"].EnumerateArray();
foreach (var workoutItemData in workoutItemsArray)
{
// Only create workout item if exerciseDay is provided
if (workoutItemData.TryGetProperty("exerciseDay", out var nameElement) &&
!string.IsNullOrEmpty(nameElement.GetString()))
{
string? exercisesJson = null;
if (workoutItemData.TryGetProperty("exercises", out var exercisesElement))
{
if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array ||
exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
exercisesJson = exercisesElement.GetRawText();
}
else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String)
{
exercisesJson = exercisesElement.GetString();
}
}
var workoutItem = new WorkoutItem
{
ExerciseDay = nameElement.GetString()!,
Description = workoutItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null,
Exercises = exercisesJson,
HowToDo = GetOptionalJsonText(workoutItemData, "howToDo"),
DoURL = GetOptionalJsonText(workoutItemData, "doURL", "videoUrl", "videoURL"),
Duration = GetOptionalJsonText(workoutItemData, "duration"),
WormUp = GetOptionalJsonText(workoutItemData, "wormUp", "warmUp", "warmup"),
Cooldown = GetOptionalJsonText(workoutItemData, "cooldown", "coolDown"),
FKWorkoutPlanId = workoutPlanId,
CreateDate = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
workoutItem.WorkoutExercises = ParseWorkoutExercises(workoutItemData);
await _repository.CreateWorkoutItemAsync(workoutItem);
}
}
}
await _uow.CommitAsync();
try
{
await _activityLogService.LogActivity(
new { description = $"Workout plan updated with ID {workoutPlanId}" },
httpContext
);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
var updatedWorkoutPlan = await _repository.GetWorkoutPlanByIdAsync(workoutPlanId);
if (updatedWorkoutPlan == null)
throw new KeyNotFoundException($"Workout plan with ID {workoutPlanId} not found after update");
// Transform workout items to have exercises as objects instead of JSON strings
var transformedWorkoutItems = updatedWorkoutPlan.WorkoutItems?.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises; // Keep as string if parsing fails
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.WorkoutExercises,
item.TotalExercises,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList();
return new
{
updatedWorkoutPlan.WorkoutPlanId,
updatedWorkoutPlan.Name,
updatedWorkoutPlan.Description,
updatedWorkoutPlan.IsTemplate,
updatedWorkoutPlan.WorkoutCategoryId,
updatedWorkoutPlan.WorkoutCategoryName,
updatedWorkoutPlan.ClientId,
updatedWorkoutPlan.ClientName,
updatedWorkoutPlan.IsActive,
updatedWorkoutPlan.CreateDate,
WorkoutItems = transformedWorkoutItems,
AssignedCustomers = updatedWorkoutPlan.AssignedCustomers
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteWorkoutPlan(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("workoutPlanId"))
{
throw new KeyNotFoundException("workoutPlanId is required");
}
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
var result = await _repository.DeleteWorkoutPlanAsync(workoutPlanId);
if (!result)
{
throw new KeyNotFoundException("Workout plan not found");
}
await _uow.CommitAsync();
try
{
await _activityLogService.LogActivity(
new { description = $"Workout plan deleted with ID {workoutPlanId}" },
httpContext
);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return new { success = true, message = "Workout plan deleted successfully" };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// Workout Item operations
public async Task<object> GetWorkoutItemsByWorkoutPlanId(object payload, HttpContext httpContext)
{
try
{
var data = DeserializePayload.Deserialize(payload);
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
var workoutItems = await _repository.GetWorkoutItemsByWorkoutPlanIdAsync(workoutPlanId);
// Transform workout items to have exercises as objects instead of JSON strings
var transformedWorkoutItems = workoutItems.Select(item =>
{
object? exercisesObj = null;
if (!string.IsNullOrEmpty(item.Exercises))
{
try
{
exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(item.Exercises);
}
catch
{
exercisesObj = item.Exercises; // Keep as string if parsing fails
}
}
return new
{
item.WorkoutItemId,
item.ExerciseDay,
item.Description,
Exercises = exercisesObj,
item.WorkoutExercises,
item.TotalExercises,
item.HowToDo,
item.DoURL,
item.Duration,
item.WormUp,
item.Cooldown,
item.WorkoutPlanId,
item.IsActive,
item.CreateDate,
item.UpdatedAt
};
}).ToList();
return transformedWorkoutItems;
}
catch
{
throw;
}
}
public async Task<object> CreateWorkoutItem(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
// Handle exercises JSON
string? exercisesJson = null;
if (data.ContainsKey("exercises"))
{
var exercisesElement = data["exercises"];
if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Array ||
exercisesElement.ValueKind == System.Text.Json.JsonValueKind.Object)
{
exercisesJson = exercisesElement.GetRawText();
}
else if (exercisesElement.ValueKind == System.Text.Json.JsonValueKind.String)
{
exercisesJson = exercisesElement.GetString();
}
}
var workoutItem = new WorkoutItem
{
ExerciseDay = data["exerciseDay"].GetString()!,
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
Exercises = exercisesJson,
HowToDo = GetOptionalJsonText(data, "howToDo"),
DoURL = GetOptionalJsonText(data, "doURL", "videoUrl", "videoURL"),
Duration = GetOptionalJsonText(data, "duration"),
WormUp = GetOptionalJsonText(data, "wormUp", "warmUp", "warmup"),
Cooldown = GetOptionalJsonText(data, "cooldown", "coolDown"),
FKWorkoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!),
CreateDate = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
workoutItem.WorkoutExercises = ParseWorkoutExercises(data);
await _repository.CreateWorkoutItemAsync(workoutItem);
await _uow.CommitAsync();
var structuredExercises = workoutItem.WorkoutExercises
.OrderBy(we => we.SortOrder)
.Select(we => new
{
we.WorkoutExerciseId,
we.Name,
we.MuscleGroup,
we.Sets,
we.Reps,
we.Description,
we.BreathIn,
we.BreathOut,
we.VideoUrl,
we.SortOrder
})
.ToList();
// Parse legacy Exercises JSON if present (kept for backward compatibility)
if (!string.IsNullOrEmpty(workoutItem.Exercises))
{
try
{
var exercisesObj = System.Text.Json.JsonSerializer.Deserialize<object>(workoutItem.Exercises);
return new
{
workoutItem.WorkoutItemId,
workoutItem.ExerciseDay,
workoutItem.Description,
Exercises = exercisesObj,
WorkoutExercises = structuredExercises,
workoutItem.HowToDo,
workoutItem.DoURL,
workoutItem.FKWorkoutPlanId,
workoutItem.IsActive,
workoutItem.CreateDate,
workoutItem.UpdatedAt
};
}
catch
{
// If parsing fails, return as-is
}
}
try
{
await _activityLogService.LogActivity(new { description = $"Workout item created with ID {workoutItem.WorkoutItemId}" }, httpContext);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return new
{
workoutItem.WorkoutItemId,
workoutItem.ExerciseDay,
workoutItem.Description,
workoutItem.Exercises,
WorkoutExercises = structuredExercises,
workoutItem.HowToDo,
workoutItem.DoURL,
workoutItem.Duration,
workoutItem.WormUp,
workoutItem.Cooldown,
workoutItem.FKWorkoutPlanId,
workoutItem.IsActive,
workoutItem.CreateDate,
workoutItem.UpdatedAt
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> AssignWorkoutToCustomer(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid workoutPlanId = Guid.Parse(data["workoutPlanId"].GetString()!);
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
var customerWorkout = new CustomerWorkout
{
FKCustomerId = customerId,
FKWorkoutPlanId = workoutPlanId,
AssignedDate = DateTime.UtcNow,
Notes = data.ContainsKey("notes") ? data["notes"].GetString() : null,
IsActive = true
};
await _repository.AssignWorkoutToCustomerAsync(customerWorkout);
await _uow.CommitAsync();
try
{
await _activityLogService.LogActivity(
new { description = $"Workout plan {workoutPlanId} assigned to customer {customerId}" },
httpContext
);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
try
{
var plan = await _repository.GetWorkoutPlanEntityByIdAsync(workoutPlanId);
var planName = plan?.WorkoutPlanName ?? "a workout plan";
await _pushNotificationService.NotifyCustomerAsync(
customerId,
"New workout plan assigned",
$"Your coach assigned you \"{planName}\". Tap to start.",
"WorkoutPlanAssigned",
"WorkoutFollowedDetail",
new { workoutPlanId }
);
}
catch (Exception notifyEx)
{
Console.WriteLine($"Push notification failed (non-critical): {notifyEx.Message}");
}
return new
{
customerWorkout.CustomerWorkoutId,
customerWorkout.FKCustomerId,
customerWorkout.FKWorkoutPlanId,
customerWorkout.AssignedDate,
customerWorkout.Notes,
customerWorkout.IsActive
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UnassignWorkoutFromCustomer(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
Guid customerWorkoutId = Guid.Parse(data["customerWorkoutId"].GetString()!);
var result = await _repository.UnassignWorkoutFromCustomerAsync(customerWorkoutId);
if (!result)
{
throw new NotFoundException($"Customer workout assignment with ID {customerWorkoutId} not found");
}
await _uow.CommitAsync();
try
{
await _activityLogService.LogActivity(
new { description = $"Workout assignment {customerWorkoutId} removed" },
httpContext
);
}
catch (Exception logEx)
{
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
}
return new { success = true, message = "Workout unassigned from customer successfully" };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
}
}