1181 lines
50 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|