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

460 lines
20 KiB
C#

using Fitness_core_BE.Models;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.DTOs.Workout;
using Microsoft.EntityFrameworkCore;
namespace Fitness_core_BE.Repos
{
public class WorkoutRepository : IWorkoutRepository
{
// Client-authored (non-template) workout plans + all CustomerWorkout assignment rows.
private readonly TenantDbContext _dbContext;
// Admin-authored templates (WorkoutCategories, and WorkoutPlans/Items/Exercises where IsTemplate=true).
private readonly CentralDbContext _central;
public WorkoutRepository(TenantDbContext dbContext, CentralDbContext central)
{
_dbContext = dbContext;
_central = central;
}
// Workout Category operations -- global lookup, admin-owned, always central.
public async Task<List<WorkoutCategoryDto>> GetWorkoutCategoryListAsync(CancellationToken ct = default)
{
return await _central.WorkoutCategories
.AsNoTracking()
.Where(wc => wc.IsActive)
.OrderBy(wc => wc.WorkoutCategoryName)
.Select(wc => new WorkoutCategoryDto
{
WorkoutCategoryId = wc.WorkoutCategoryId,
Name = wc.WorkoutCategoryName,
Description = wc.Description,
IsActive = wc.IsActive,
CreateDate = wc.CreateDate
})
.ToListAsync(ct);
}
// Categories are central; the plan row is tenant-side with no enforceable cross-DB
// FK, so the id has to be checked here rather than left to the database.
public Task<bool> WorkoutCategoryExistsAsync(Guid workoutCategoryId, CancellationToken ct = default)
{
return _central.WorkoutCategories
.AsNoTracking()
.AnyAsync(wc => wc.WorkoutCategoryId == workoutCategoryId && wc.IsActive, ct);
}
public async Task<WorkoutCategory> CreateWorkoutCategoryAsync(WorkoutCategory workoutCategory, CancellationToken ct = default)
{
await _central.WorkoutCategories.AddAsync(workoutCategory, ct);
return workoutCategory;
}
// Workout Plan operations
public async Task<List<WorkoutPlanDto>> GetWorkoutPlanListAsync(string userType, Guid clientId, CancellationToken ct = default)
{
// A customer browsing sees admin-authored templates (central); the coach/owner
// dashboard only manages this gym's own custom plans (tenant) -- disjoint sets
// post-split, never merged in one query.
if (userType == "Customer")
{
return await _central.WorkoutPlans
.AsNoTracking()
.Include(wp => wp.WorkoutCategory)
.Where(wp => wp.IsActive && wp.IsTemplate)
.OrderByDescending(wp => wp.CreateDate)
.Select(wp => new WorkoutPlanDto
{
WorkoutPlanId = wp.WorkoutPlanId,
Name = wp.WorkoutPlanName,
Description = wp.Description,
IsTemplate = wp.IsTemplate,
WorkoutCategoryId = wp.FKWorkoutCategoryId,
WorkoutCategoryName = wp.WorkoutCategory != null ? wp.WorkoutCategory.WorkoutCategoryName : "",
ClientId = wp.FKClientId,
ClientName = null,
IsActive = wp.IsActive,
CreateDate = wp.CreateDate,
AssignedCustomers = new List<CustomerWorkoutDto>()
})
.ToListAsync(ct);
}
// Categories live centrally, so the tenant query can't join them -- resolve the
// names in a second round-trip and stitch them on below.
var categoryNames = await _central.WorkoutCategories
.AsNoTracking()
.ToDictionaryAsync(wc => wc.WorkoutCategoryId, wc => wc.WorkoutCategoryName, ct);
var tenantPlans = await _dbContext.WorkoutPlans
.AsNoTracking()
.Include(wp => wp.CustomerWorkouts.Where(cw => cw.IsActive))
.ThenInclude(cw => cw.TenantCustomerLink)
.Where(wp => wp.IsActive && !wp.IsTemplate && wp.FKClientId == clientId)
.OrderByDescending(wp => wp.CreateDate)
.Select(wp => new WorkoutPlanDto
{
WorkoutPlanId = wp.WorkoutPlanId,
Name = wp.WorkoutPlanName,
Description = wp.Description,
IsTemplate = wp.IsTemplate,
WorkoutCategoryId = wp.FKWorkoutCategoryId,
WorkoutCategoryName = "",
ClientId = wp.FKClientId,
ClientName = null,
IsActive = wp.IsActive,
CreateDate = wp.CreateDate,
AssignedCustomers = wp.CustomerWorkouts
.Where(cw => cw.IsActive)
.Select(cw => new CustomerWorkoutDto
{
CustomerWorkoutId = cw.CustomerWorkoutId,
CustomerId = cw.FKCustomerId,
CustomerName = cw.TenantCustomerLink != null ? cw.TenantCustomerLink.DisplayName : null,
WorkoutPlanId = cw.FKWorkoutPlanId,
IsActive = cw.IsActive,
IsFollowing = cw.IsFollowing,
AssignedDate = cw.AssignedDate,
CompletedDate = cw.CompletedDate,
Notes = cw.Notes
}).ToList()
})
.ToListAsync(ct);
foreach (var plan in tenantPlans)
{
plan.WorkoutCategoryName =
categoryNames.TryGetValue(plan.WorkoutCategoryId, out var name) ? name : "";
}
return tenantPlans;
}
public async Task<WorkoutPlanDetailDto?> GetWorkoutPlanByIdAsync(Guid id, CancellationToken ct = default)
{
// The id could belong to either bucket (an admin template or a client-owned
// plan) -- try central first, fall back to tenant.
var centralPlan = await _central.WorkoutPlans
.AsNoTracking()
.Include(wp => wp.WorkoutCategory)
.Include(wp => wp.WorkoutItems)
.ThenInclude(wi => wi.WorkoutExercises)
.FirstOrDefaultAsync(wp => wp.WorkoutPlanId == id && wp.IsActive, ct);
if (centralPlan != null)
return MapDetail(centralPlan, Array.Empty<CustomerWorkout>());
var tenantPlan = await _dbContext.WorkoutPlans
.AsNoTracking()
.Include(wp => wp.CustomerWorkouts.Where(cw => cw.IsActive))
.ThenInclude(cw => cw.TenantCustomerLink)
.Include(wp => wp.WorkoutItems)
.ThenInclude(wi => wi.WorkoutExercises)
.FirstOrDefaultAsync(wp => wp.WorkoutPlanId == id && wp.IsActive, ct);
if (tenantPlan == null)
return null;
var detail = MapDetail(tenantPlan, tenantPlan.CustomerWorkouts.Where(cw => cw.IsActive).ToList());
// Category is central-only, so it can't be Include()d from the tenant context.
detail.WorkoutCategoryName = await _central.WorkoutCategories
.AsNoTracking()
.Where(wc => wc.WorkoutCategoryId == tenantPlan.FKWorkoutCategoryId)
.Select(wc => wc.WorkoutCategoryName)
.FirstOrDefaultAsync(ct) ?? "";
return detail;
}
private static WorkoutPlanDetailDto MapDetail(WorkoutPlan workoutPlan, IReadOnlyCollection<CustomerWorkout> assigned)
{
return new WorkoutPlanDetailDto
{
WorkoutPlanId = workoutPlan.WorkoutPlanId,
Name = workoutPlan.WorkoutPlanName,
Description = workoutPlan.Description,
IsTemplate = workoutPlan.IsTemplate,
WorkoutCategoryId = workoutPlan.FKWorkoutCategoryId,
WorkoutCategoryName = workoutPlan.WorkoutCategory != null ? workoutPlan.WorkoutCategory.WorkoutCategoryName : "",
ClientId = workoutPlan.FKClientId,
ClientName = null,
IsActive = workoutPlan.IsActive,
CreateDate = workoutPlan.CreateDate,
WorkoutItems = workoutPlan.WorkoutItems
.Where(wi => wi.IsActive)
.Select(MapWorkoutItem)
.ToList(),
AssignedCustomers = assigned
.Select(cw => new CustomerWorkoutDto
{
CustomerWorkoutId = cw.CustomerWorkoutId,
CustomerId = cw.FKCustomerId,
CustomerName = cw.TenantCustomerLink?.DisplayName,
WorkoutPlanId = cw.FKWorkoutPlanId,
IsActive = cw.IsActive,
IsFollowing = cw.IsFollowing,
AssignedDate = cw.AssignedDate,
CompletedDate = cw.CompletedDate,
Notes = cw.Notes
}).ToList()
};
}
// Shared WorkoutItem -> WorkoutItemDto mapping (including structured exercises) for the
// client/coach-facing read paths, which operate on already-materialized entity graphs
// rather than server-translated queries.
private static WorkoutItemDto MapWorkoutItem(WorkoutItem wi)
{
var exerciseDtos = wi.WorkoutExercises
.Where(we => we.IsActive)
.OrderBy(we => we.SortOrder)
.Select(we => new WorkoutExerciseDto
{
WorkoutExerciseId = we.WorkoutExerciseId,
Name = we.Name,
MuscleGroup = we.MuscleGroup,
Sets = we.Sets,
Reps = we.Reps,
Description = we.Description,
BreathIn = we.BreathIn,
BreathOut = we.BreathOut,
VideoUrl = we.VideoUrl,
SortOrder = we.SortOrder,
IsCompleted = false
})
.ToList();
return new WorkoutItemDto
{
WorkoutItemId = wi.WorkoutItemId,
ExerciseDay = wi.ExerciseDay,
Description = wi.Description,
Exercises = wi.Exercises,
HowToDo = wi.HowToDo,
DoURL = wi.DoURL,
Duration = wi.Duration,
WormUp = wi.WormUp,
Cooldown = wi.Cooldown,
WorkoutPlanId = wi.FKWorkoutPlanId,
IsActive = wi.IsActive,
CreateDate = wi.CreateDate,
UpdatedAt = wi.UpdatedAt,
WorkoutExercises = exerciseDtos,
TotalExercises = exerciseDtos.Count,
CompletedExercises = 0,
IsCompleted = false
};
}
public async Task<List<WorkoutPlanDetailDto>> GetWorkoutPlansByCustomerIdAsync(Guid customerId, CancellationToken ct = default)
{
// CustomerWorkout (tenant) records which plans this customer follows at this gym;
// each followed plan is resolved from whichever bucket it actually lives in.
var follows = await _dbContext.CustomerWorkouts
.AsNoTracking()
.Where(cw => cw.FKCustomerId == customerId && cw.IsActive)
.OrderByDescending(cw => cw.AssignedDate)
.Select(cw => cw.FKWorkoutPlanId)
.Distinct()
.ToListAsync(ct);
var result = new List<WorkoutPlanDetailDto>();
foreach (var workoutPlanId in follows)
{
var detail = await GetWorkoutPlanByIdAsync(workoutPlanId, ct);
if (detail != null && detail.IsActive)
result.Add(detail);
}
return result;
}
public async Task<WorkoutPlan?> GetWorkoutPlanEntityByIdAsync(Guid id, CancellationToken ct = default)
{
return await _dbContext.WorkoutPlans
.FirstOrDefaultAsync(wp => wp.WorkoutPlanId == id && wp.IsActive, ct);
}
public async Task<WorkoutPlan> CreateWorkoutPlanAsync(WorkoutPlan workoutPlan, CancellationToken ct = default)
{
await _dbContext.WorkoutPlans.AddAsync(workoutPlan, ct);
return workoutPlan;
}
public async Task<WorkoutPlan> UpdateWorkoutPlanAsync(WorkoutPlan workoutPlan, CancellationToken ct = default)
{
_dbContext.WorkoutPlans.Update(workoutPlan);
return workoutPlan;
}
public async Task<bool> DeleteWorkoutPlanAsync(Guid id, CancellationToken ct = default)
{
var workoutPlan = await _dbContext.WorkoutPlans.FindAsync(new object[] { id }, ct);
if (workoutPlan == null)
{
return false;
}
workoutPlan.IsActive = false;
_dbContext.WorkoutPlans.Update(workoutPlan);
return true;
}
public async Task<WorkoutItem?> GetWorkoutItemByIdAsync(Guid id, CancellationToken ct = default)
{
return await _dbContext.WorkoutItems
.FirstOrDefaultAsync(wi => wi.WorkoutItemId == id && wi.IsActive, ct);
}
// Workout Item operations
public async Task<List<WorkoutItemDto>> GetWorkoutItemsByWorkoutPlanIdAsync(Guid workoutPlanId, CancellationToken ct = default)
{
var items = await _dbContext.WorkoutItems
.AsNoTracking()
.Include(wi => wi.WorkoutExercises)
.Where(wi => wi.FKWorkoutPlanId == workoutPlanId && wi.IsActive)
.OrderBy(wi => wi.CreateDate)
.ToListAsync(ct);
return items.Select(MapWorkoutItem).ToList();
}
public async Task<WorkoutItem> CreateWorkoutItemAsync(WorkoutItem workoutItem, CancellationToken ct = default)
{
await _dbContext.WorkoutItems.AddAsync(workoutItem, ct);
return workoutItem;
}
public async Task<WorkoutItem> UpdateWorkoutItemAsync(WorkoutItem workoutItem, CancellationToken ct = default)
{
_dbContext.WorkoutItems.Update(workoutItem);
return workoutItem;
}
public async Task DeleteWorkoutItemAsync(Guid id, CancellationToken ct = default)
{
var workoutItem = await _dbContext.WorkoutItems.FindAsync(new object[] { id }, ct);
if (workoutItem != null)
{
// Soft delete by setting IsActive to false
workoutItem.IsActive = false;
_dbContext.WorkoutItems.Update(workoutItem);
}
}
// Customer Workout Assignment operations
// Assigning a plan puts the customer on it the same way self-follow does: only one
// plan can be "followed" at a time, and an assigned plan must show up under the
// customer's Followed tab immediately.
public async Task<CustomerWorkout> AssignWorkoutToCustomerAsync(CustomerWorkout customerWorkout, CancellationToken ct = default)
{
var currentFollow = await _dbContext.CustomerWorkouts
.FirstOrDefaultAsync(cw => cw.FKCustomerId == customerWorkout.FKCustomerId && cw.IsFollowing, ct);
if (currentFollow != null && currentFollow.FKWorkoutPlanId != customerWorkout.FKWorkoutPlanId)
{
currentFollow.IsFollowing = false;
_dbContext.CustomerWorkouts.Update(currentFollow);
}
var existing = await _dbContext.CustomerWorkouts
.FirstOrDefaultAsync(cw => cw.FKCustomerId == customerWorkout.FKCustomerId && cw.FKWorkoutPlanId == customerWorkout.FKWorkoutPlanId, ct);
if (existing != null)
{
existing.IsFollowing = true;
existing.IsActive = true;
existing.AssignedDate = customerWorkout.AssignedDate;
existing.Notes = customerWorkout.Notes;
_dbContext.CustomerWorkouts.Update(existing);
return existing;
}
customerWorkout.IsFollowing = true;
await _dbContext.CustomerWorkouts.AddAsync(customerWorkout, ct);
return customerWorkout;
}
public async Task<bool> UnassignWorkoutFromCustomerAsync(Guid customerWorkoutId, CancellationToken ct = default)
{
var customerWorkout = await _dbContext.CustomerWorkouts.FindAsync(new object[] { customerWorkoutId }, ct);
if (customerWorkout == null)
{
return false;
}
// Soft delete by setting IsActive to false
customerWorkout.IsActive = false;
_dbContext.CustomerWorkouts.Update(customerWorkout);
return true;
}
// A followed plan id may name a central template or a tenant plan, so names are
// resolved from both buckets rather than through a (now un-enforced) navigation.
private async Task<Dictionary<Guid, string>> ResolvePlanNamesAsync(
IReadOnlyCollection<Guid> planIds, CancellationToken ct)
{
if (planIds.Count == 0)
return new Dictionary<Guid, string>();
var names = await _dbContext.WorkoutPlans
.AsNoTracking()
.Where(wp => planIds.Contains(wp.WorkoutPlanId))
.ToDictionaryAsync(wp => wp.WorkoutPlanId, wp => wp.WorkoutPlanName, ct);
var missing = planIds.Where(id => !names.ContainsKey(id)).ToList();
if (missing.Count > 0)
{
var centralNames = await _central.WorkoutPlans
.AsNoTracking()
.Where(wp => missing.Contains(wp.WorkoutPlanId))
.ToDictionaryAsync(wp => wp.WorkoutPlanId, wp => wp.WorkoutPlanName, ct);
foreach (var pair in centralNames)
names[pair.Key] = pair.Value;
}
return names;
}
private async Task<List<CustomerWorkoutDto>> BuildCustomerWorkoutDtosAsync(
IQueryable<CustomerWorkout> query, CancellationToken ct)
{
var rows = await query
.AsNoTracking()
.Include(cw => cw.TenantCustomerLink)
.OrderByDescending(cw => cw.AssignedDate)
.Select(cw => new CustomerWorkoutDto
{
CustomerWorkoutId = cw.CustomerWorkoutId,
CustomerId = cw.FKCustomerId,
CustomerName = cw.TenantCustomerLink != null ? cw.TenantCustomerLink.DisplayName : null,
WorkoutPlanId = cw.FKWorkoutPlanId,
WorkoutPlanName = null,
IsActive = cw.IsActive,
IsFollowing = cw.IsFollowing,
AssignedDate = cw.AssignedDate,
CompletedDate = cw.CompletedDate,
Notes = cw.Notes
})
.ToListAsync(ct);
var names = await ResolvePlanNamesAsync(
rows.Select(r => r.WorkoutPlanId).Distinct().ToList(), ct);
foreach (var row in rows)
row.WorkoutPlanName = names.TryGetValue(row.WorkoutPlanId, out var name) ? name : null;
return rows;
}
public Task<List<CustomerWorkoutDto>> GetCustomerWorkoutsByPlanIdAsync(Guid workoutPlanId, CancellationToken ct = default)
=> BuildCustomerWorkoutDtosAsync(
_dbContext.CustomerWorkouts.Where(cw => cw.FKWorkoutPlanId == workoutPlanId && cw.IsActive), ct);
public Task<List<CustomerWorkoutDto>> GetCustomerWorkoutsByCustomerIdAsync(Guid customerId, CancellationToken ct = default)
=> BuildCustomerWorkoutDtosAsync(
_dbContext.CustomerWorkouts.Where(cw => cw.FKCustomerId == customerId && cw.IsActive), ct);
}
}