using Fitness_core_BE.DTOs.Meal; using Fitness_core_BE.DTOs.Workout; using Fitness_core_BE.Interfaces; using Fitness_core_BE.Models; using Microsoft.EntityFrameworkCore; using Fitness_core_BE.Exceptions; namespace Fitness_core_BE.Repos { public class CustomerRepository : ICustomerRepository { // Tenant-scoped operational data (memberships, plan assignments, cards, ...). private readonly TenantDbContext _context; // Central identity data (Customer, health/targets, admin templates, ClientBusiness). private readonly CentralDbContext _central; public CustomerRepository(TenantDbContext context, CentralDbContext central) { _context = context; _central = central; } public async Task GetCustomerById(Guid customerId) { return await _central.Customer .Include(c => c.User) .FirstOrDefaultAsync(c => c.CusId == customerId && c.IsActive); } public async Task GetCustomerByIdentifier(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) return null; var id = identifier.Trim(); return await _central.Customer .Include(c => c.User) .FirstOrDefaultAsync(c => c.PrimaryPH == id || c.SecondaryPH == id || c.Email == id || (c.User != null && (c.User.PhoneNumber == id || c.User.Email == id))); } public async Task CreateCustomer(Customer customer) { if (customer.User != null) { _central.Users.Add(customer.User); } else { var userExists = await _central.Users.AnyAsync(u => u.UserId == customer.FKUserId); if (!userExists) { throw new Exception("User not found"); } } _central.Customer.Add(customer); return customer; } // Every tenant-side customer FK (CustomerSubscriptions, SubscriptionMembers, cards, // attendance, ...) targets TenantCustomerLink, not the central Customer table, so the // anchor row has to exist in this tenant DB before any of them can be inserted. The // link id IS the central Customer.CusId (see Models/TenantCustomerLink.cs), so this is // a pure upsert keyed on that id -- no surrogate key to allocate. public async Task EnsureTenantCustomerLink(Customer customer) { var userId = customer.User?.UserId ?? customer.FKUserId; // Local-first: on the create path the Customer/User rows are still pending in the // central context, and the link row may itself already be pending from an earlier // call in this same unit of work -- FindAsync checks the change tracker before // hitting the DB, so a second call in one request updates rather than re-adds. var link = await _context.TenantCustomerLink.FindAsync(customer.CusId); if (link == null) { _context.TenantCustomerLink.Add(new TenantCustomerLink { TenantCustomerLinkId = customer.CusId, CentralUserId = userId, DisplayName = customer.CusName, PrimaryPhoneCached = customer.PrimaryPH, EmailCached = customer.Email, IsActiveAtThisClient = true, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); return; } // Re-enrolment at this gym: revive the link and refresh the cached display fields // so list/report queries that read them don't serve stale values. link.CentralUserId = userId; link.DisplayName = customer.CusName; link.PrimaryPhoneCached = customer.PrimaryPH; link.EmailCached = customer.Email; link.IsActiveAtThisClient = true; link.UpdatedAt = DateTime.UtcNow; } // Own-gym duplicate check for customer creation: this tenant's own TenantCustomerLink // table already holds exactly this gym's active customers, so no clientId is needed. public async Task FindActiveLinkByIdentifierAsync(string identifier) { if (string.IsNullOrWhiteSpace(identifier)) return null; var id = identifier.Trim(); return await _context.TenantCustomerLink .FirstOrDefaultAsync(l => l.IsActiveAtThisClient && (l.PrimaryPhoneCached == id || l.EmailCached == id)); } public async Task UpdateCustomer(Guid customerId, Customer updatedCustomer) { // updatedCustomer is the same tracked instance GetCustomerById(customerId) returned // (Customer + Include(User)), so any field the caller set directly on it — including // updatedCustomer.User.* — is already picked up by SaveChanges via change tracking. _central.Customer.Update(updatedCustomer); return updatedCustomer; } // Self-service full-account delete (legacy generic-dispatch path, CustomerService. // DeleteCustomer): unlike the client-portal delete below, this really does deactivate // the shared central identity -- the customer themselves is asking to leave the // platform entirely, not just one gym. public async Task DeleteCustomer(Guid customerId) { var customer = await _central.Customer.FindAsync(customerId); if (customer == null) { return false; } customer.IsActive = false; // A deleted customer must no longer retain an active membership. CustomerPlan is // a reusable plan definition, so do not deactivate/delete it; only deactivate the // customer's subscription and its active related records. var ownedSubscriptionIds = await _context.CustomerSubscriptions .Where(subscription => subscription.FKCustomerId == customerId) .Select(subscription => subscription.SubscriptionId) .ToListAsync(); var ownedSubscriptions = await _context.CustomerSubscriptions .Where(subscription => ownedSubscriptionIds.Contains(subscription.SubscriptionId) && subscription.IsActive) .ToListAsync(); foreach (var subscription in ownedSubscriptions) subscription.IsActive = false; var membersToDeactivate = await _context.SubscriptionMembers .Where(member => member.IsActive && (member.FKCustomerId == customerId || ownedSubscriptionIds.Contains(member.FKSubscriptionId))) .ToListAsync(); foreach (var member in membersToDeactivate) member.IsActive = false; var subItemsToDeactivate = await _context.CustomerSubItems .Where(item => item.IsActive && ownedSubscriptionIds.Contains(item.FKCustomerSubscriptionId)) .ToListAsync(); foreach (var item in subItemsToDeactivate) item.IsActive = false; var installmentsToDeactivate = await _context.SubscriptionInstallments .Where(installment => installment.IsActive && ownedSubscriptionIds.Contains(installment.FKSubscriptionId)) .ToListAsync(); foreach (var installment in installmentsToDeactivate) installment.IsActive = false; // A deleted customer must also drop off their assigned workout/meal plans. If this // customer is ever re-added later (see CreateCustomerWithPlans reactivation), these // rows stay inactive — re-adding assigns fresh plans rather than reviving old ones. var workoutsToDeactivate = await _context.CustomerWorkouts .Where(cw => cw.IsActive && cw.FKCustomerId == customerId) .ToListAsync(); foreach (var workout in workoutsToDeactivate) workout.IsActive = false; var mealsToDeactivate = await _context.CustomerMeals .Where(cm => cm.IsActive && cm.FKCustomerId == customerId) .ToListAsync(); foreach (var meal in mealsToDeactivate) meal.IsActive = false; return true; } // Client-initiated delete: only deactivates this customer's subscriptions/plan // assignments at the calling client's gym — the shared central Customer.IsActive flag // is never touched here, since the same person may still be an active member elsewhere. public async Task DeactivateCustomerForClient(Guid customerId, Guid clientId) { var customerExists = await _central.Customer.AnyAsync(c => c.CusId == customerId); if (!customerExists) return false; var ownedSubscriptionIds = await _context.CustomerSubscriptions .Where(subscription => subscription.FKCustomerId == customerId && subscription.CustomerPlan.FKClientId == clientId) .Select(subscription => subscription.SubscriptionId) .ToListAsync(); var ownedSubscriptions = await _context.CustomerSubscriptions .Where(subscription => ownedSubscriptionIds.Contains(subscription.SubscriptionId) && subscription.IsActive) .ToListAsync(); foreach (var subscription in ownedSubscriptions) subscription.IsActive = false; var membersToDeactivate = await _context.SubscriptionMembers .Where(member => member.IsActive && ownedSubscriptionIds.Contains(member.FKSubscriptionId)) .ToListAsync(); foreach (var member in membersToDeactivate) member.IsActive = false; var subItemsToDeactivate = await _context.CustomerSubItems .Where(item => item.IsActive && ownedSubscriptionIds.Contains(item.FKCustomerSubscriptionId)) .ToListAsync(); foreach (var item in subItemsToDeactivate) item.IsActive = false; var installmentsToDeactivate = await _context.SubscriptionInstallments .Where(installment => installment.IsActive && ownedSubscriptionIds.Contains(installment.FKSubscriptionId)) .ToListAsync(); foreach (var installment in installmentsToDeactivate) installment.IsActive = false; // Only drop workout/meal assignments tied to this client's own plan, or to a // shared admin template (FKClientId == null) — never another client's custom plan. // The plan may live in either bucket (templates are central), so the eligible ids // are gathered from both and matched on the scalar FK. var eligibleWorkoutPlanIds = await _context.WorkoutPlans .Where(wp => wp.FKClientId == clientId || wp.FKClientId == null) .Select(wp => wp.WorkoutPlanId) .ToListAsync(); eligibleWorkoutPlanIds.AddRange(await _central.WorkoutPlans .Where(wp => wp.FKClientId == clientId || wp.FKClientId == null) .Select(wp => wp.WorkoutPlanId) .ToListAsync()); var workoutsToDeactivate = await _context.CustomerWorkouts .Where(cw => cw.IsActive && cw.FKCustomerId == customerId && eligibleWorkoutPlanIds.Contains(cw.FKWorkoutPlanId)) .ToListAsync(); foreach (var workout in workoutsToDeactivate) workout.IsActive = false; var eligibleMealPlanIds = await _context.MealPlans .Where(mp => mp.FKClientId == clientId || mp.FKClientId == null) .Select(mp => mp.MealPlanId) .ToListAsync(); eligibleMealPlanIds.AddRange(await _central.MealPlans .Where(mp => mp.FKClientId == clientId || mp.FKClientId == null) .Select(mp => mp.MealPlanId) .ToListAsync()); var mealsToDeactivate = await _context.CustomerMeals .Where(cm => cm.IsActive && cm.FKCustomerId == customerId && eligibleMealPlanIds.Contains(cm.FKMealPlanId)) .ToListAsync(); foreach (var meal in mealsToDeactivate) meal.IsActive = false; return true; } public async Task GetCustomerClientId(Guid customerId) { CustomerSubscription? cusSub = await _context.CustomerSubscriptions .Include(CustomerPlan => CustomerPlan.CustomerPlan) .Where(cs => cs.FKCustomerId == customerId && cs.IsActive) .FirstOrDefaultAsync(); if (cusSub == null) return null; return cusSub.CustomerPlan.FKClientId; } // A MealPlanId/WorkoutPlanId referenced from a tenant CustomerMeal/CustomerWorkout row // could be an admin template (central) or this gym's own custom plan (tenant) -- try // central first, fall back to tenant. private async Task ResolveMealPlanAsync(Guid mealPlanId, bool includeItems = false) { IQueryable centralQuery = _central.MealPlans.AsNoTracking(); if (includeItems) centralQuery = centralQuery.Include(mp => mp.MealItems); var central = await centralQuery.FirstOrDefaultAsync(mp => mp.MealPlanId == mealPlanId); if (central != null) return central; IQueryable tenantQuery = _context.MealPlans.AsNoTracking(); if (includeItems) tenantQuery = tenantQuery.Include(mp => mp.MealItems); return await tenantQuery.FirstOrDefaultAsync(mp => mp.MealPlanId == mealPlanId); } private async Task ResolveWorkoutPlanAsync(Guid workoutPlanId, bool includeItems = false) { IQueryable centralQuery = _central.WorkoutPlans.AsNoTracking(); if (includeItems) centralQuery = centralQuery.Include(wp => wp.WorkoutItems).ThenInclude(wi => wi.WorkoutExercises); var central = await centralQuery.FirstOrDefaultAsync(wp => wp.WorkoutPlanId == workoutPlanId); if (central != null) return central; IQueryable tenantQuery = _context.WorkoutPlans.AsNoTracking(); if (includeItems) tenantQuery = tenantQuery.Include(wp => wp.WorkoutItems).ThenInclude(wi => wi.WorkoutExercises); return await tenantQuery.FirstOrDefaultAsync(wp => wp.WorkoutPlanId == workoutPlanId); } public async Task GetActiveMealPlanAsync(Guid? clientId = null, Guid? cusId = null) { if (clientId == null && cusId == null) throw new ArgumentException("Either clientId or cusId must be provided."); Guid? mealPlanId; if (clientId != null) { // The plan may be a central template or this gym's own, so match on the // scalar id against plan ids gathered from both buckets. var clientMealPlanIds = await _context.MealPlans .Where(mp => mp.FKClientId == clientId) .Select(mp => mp.MealPlanId) .ToListAsync(); clientMealPlanIds.AddRange(await _central.MealPlans .Where(mp => mp.FKClientId == clientId) .Select(mp => mp.MealPlanId) .ToListAsync()); mealPlanId = await _context.CustomerMeals .Where(cm => clientMealPlanIds.Contains(cm.FKMealPlanId) && cm.IsActive && cm.FKCustomerId == cusId) .Select(cm => (Guid?)cm.FKMealPlanId) .FirstOrDefaultAsync(); } else { mealPlanId = await _context.CustomerMeals .Where(cm => cm.FKCustomerId == cusId && cm.IsActive) .Select(cm => (Guid?)cm.FKMealPlanId) .FirstOrDefaultAsync(); } return mealPlanId.HasValue ? await ResolveMealPlanAsync(mealPlanId.Value) : null; } // Shared plan-detail assembly for the meal-plan read paths below: resolves each plan // from whichever bucket it lives in, then batches this tenant's CustomerMeal // assignment rows for those plans (assignment rows are always tenant-local, even when // the plan itself is a central template). private async Task> BuildMealPlanDetailsAsync( IReadOnlyCollection mealPlanIds, Guid? assignedCustomerFilter) { if (mealPlanIds.Count == 0) return new List(); var centralPlans = await _central.MealPlans .AsNoTracking() .Include(mp => mp.MealCategory) .Include(mp => mp.MealItems) .Where(mp => mealPlanIds.Contains(mp.MealPlanId) && mp.IsActive) .ToListAsync(); var foundIds = centralPlans.Select(mp => mp.MealPlanId).ToHashSet(); var missingIds = mealPlanIds.Where(id => !foundIds.Contains(id)).ToList(); var tenantPlans = missingIds.Count == 0 ? new List() : await _context.MealPlans .AsNoTracking() .Include(mp => mp.MealCategory) .Include(mp => mp.MealItems) .Where(mp => missingIds.Contains(mp.MealPlanId) && mp.IsActive) .ToListAsync(); var plans = centralPlans.Concat(tenantPlans).ToList(); if (plans.Count == 0) return new List(); var assignmentQuery = _context.CustomerMeals .AsNoTracking() .Include(cm => cm.TenantCustomerLink) .Where(cm => plans.Select(p => p.MealPlanId).Contains(cm.FKMealPlanId) && cm.IsActive); if (assignedCustomerFilter.HasValue) assignmentQuery = assignmentQuery.Where(cm => cm.FKCustomerId == assignedCustomerFilter.Value); var assignments = await assignmentQuery.ToListAsync(); var assignmentsByPlan = assignments.GroupBy(a => a.FKMealPlanId).ToDictionary(g => g.Key, g => g.ToList()); return plans.Select(mealPlan => { var mealItems = mealPlan.MealItems .Where(mi => mi.IsActive) .Select(mi => new MealDto { MealId = mi.MealId, Name = mi.Name, Description = mi.Description, Calories = mi.Calories, Protein = mi.Protein, Carbohydrates = mi.Carbohydrates, Fats = mi.Fats, ServingSize = mi.ServingSize, MealItems = mi.MealItems, PreparationNotes = mi.PreparationNotes, MealPlanId = mi.FKMealPlanId, IsActive = mi.IsActive, CreateDate = mi.CreateDate }) .ToList(); assignmentsByPlan.TryGetValue(mealPlan.MealPlanId, out var planAssignments); return new MealPlanDetailDto { MealPlanId = mealPlan.MealPlanId, Name = mealPlan.Name, Description = mealPlan.Description, IsTemplate = mealPlan.IsTemplate, MealCategoryId = mealPlan.FKMealCategoryId, MealCategoryName = mealPlan.MealCategory != null ? mealPlan.MealCategory.Name : "", ClientId = mealPlan.FKClientId, ClientName = null, IsActive = mealPlan.IsActive, CreateDate = mealPlan.CreateDate, MealItems = mealItems, AssignedCustomers = (planAssignments ?? new List()) .Select(cm => new CustomerMealDto { CustomerMealId = cm.CustomerMealId, CustomerId = cm.FKCustomerId, CustomerName = cm.TenantCustomerLink?.DisplayName, MealPlanId = cm.FKMealPlanId, IsActive = cm.IsActive, IsFollowing = cm.IsFollowing, AssignedDate = cm.AssignedDate, CompletedDate = cm.CompletedDate, Notes = cm.Notes }).ToList(), TotalCalories = mealItems.Sum(mi => mi.Calories ?? 0), TotalProtein = mealItems.Sum(mi => mi.Protein ?? 0), TotalCarbohydrates = mealItems.Sum(mi => mi.Carbohydrates ?? 0), TotalFats = mealItems.Sum(mi => mi.Fats ?? 0) }; }).ToList(); } public async Task> GetFollowedMealPlansByCustomerIdAsync(Guid customerId) { var planIds = await _context.CustomerMeals .AsNoTracking() .Where(cm => cm.FKCustomerId == customerId && cm.IsActive && cm.IsFollowing) .OrderByDescending(cm => cm.AssignedDate) .Select(cm => cm.FKMealPlanId) .Distinct() .ToListAsync(); return await BuildMealPlanDetailsAsync(planIds, customerId); } // All of the customer's own AI-generated meal plans, whether currently followed or // not — i.e. every non-template plan this customer has ever created/been assigned via // CreateCustomerMealPlan. Distinct from admin/client-authored templates (IsTemplate=true). public async Task> GetMyMealPlansByCustomerIdAsync(Guid customerId) { var planIds = await _context.CustomerMeals .AsNoTracking() .Where(cm => cm.FKCustomerId == customerId && cm.IsActive) .OrderByDescending(cm => cm.AssignedDate) .Select(cm => cm.FKMealPlanId) .Distinct() .ToListAsync(); var details = await BuildMealPlanDetailsAsync(planIds, customerId); return details.Where(d => !d.IsTemplate).ToList(); } public async Task GetMealPlanByIdForCustomerAsync(Guid mealPlanId) { var details = await BuildMealPlanDetailsAsync(new[] { mealPlanId }, assignedCustomerFilter: null); return details.FirstOrDefault(); } // Projects a customer's completed WorkoutExerciseIds, keyed by CustomerWorkoutId, for // the given set of CustomerWorkout rows — used to stamp IsCompleted on exercise DTOs. private async Task>> GetCompletedExerciseIdsByCustomerWorkoutAsync(IEnumerable customerWorkoutIds) { var ids = customerWorkoutIds.Distinct().ToList(); if (ids.Count == 0) return new Dictionary>(); var completions = await _context.CustomerWorkoutExerciseCompletions .AsNoTracking() .Where(c => ids.Contains(c.FKCustomerWorkoutId)) .Select(c => new { c.FKCustomerWorkoutId, c.FKWorkoutExerciseId }) .ToListAsync(); return completions .GroupBy(c => c.FKCustomerWorkoutId) .ToDictionary(g => g.Key, g => g.Select(c => c.FKWorkoutExerciseId).ToHashSet()); } private static List MapWorkoutItemsWithExercises(IEnumerable items, HashSet? completedExerciseIds, bool onlyActive = true) { var completed = completedExerciseIds ?? new HashSet(); var filtered = onlyActive ? items.Where(wi => wi.IsActive) : items; return filtered.Select(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 = completed.Contains(we.WorkoutExerciseId) }) .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 = exerciseDtos.Count(e => e.IsCompleted), IsCompleted = exerciseDtos.Count > 0 && exerciseDtos.All(e => e.IsCompleted) }; }).ToList(); } // Shared plan-detail assembly for the workout-plan read paths below -- same // central-or-tenant resolution + tenant-only assignment batching as meal plans. private async Task> BuildWorkoutPlanDetailsAsync( IReadOnlyCollection workoutPlanIds, Guid? assignedCustomerFilter) { if (workoutPlanIds.Count == 0) return new List(); var centralPlans = await _central.WorkoutPlans .AsNoTracking() .Include(wp => wp.WorkoutCategory) .Include(wp => wp.WorkoutItems).ThenInclude(wi => wi.WorkoutExercises) .Where(wp => workoutPlanIds.Contains(wp.WorkoutPlanId) && wp.IsActive) .ToListAsync(); var foundIds = centralPlans.Select(wp => wp.WorkoutPlanId).ToHashSet(); var missingIds = workoutPlanIds.Where(id => !foundIds.Contains(id)).ToList(); var tenantPlans = missingIds.Count == 0 ? new List() : await _context.WorkoutPlans .AsNoTracking() .Include(wp => wp.WorkoutCategory) .Include(wp => wp.WorkoutItems).ThenInclude(wi => wi.WorkoutExercises) .Where(wp => missingIds.Contains(wp.WorkoutPlanId) && wp.IsActive) .ToListAsync(); var plans = centralPlans.Concat(tenantPlans).ToList(); if (plans.Count == 0) return new List(); var assignmentQuery = _context.CustomerWorkouts .AsNoTracking() .Include(cw => cw.TenantCustomerLink) .Where(cw => plans.Select(p => p.WorkoutPlanId).Contains(cw.FKWorkoutPlanId) && cw.IsActive); if (assignedCustomerFilter.HasValue) assignmentQuery = assignmentQuery.Where(cw => cw.FKCustomerId == assignedCustomerFilter.Value); var assignments = await assignmentQuery.ToListAsync(); var assignmentsByPlan = assignments.GroupBy(a => a.FKWorkoutPlanId).ToDictionary(g => g.Key, g => g.ToList()); var completedByWorkout = await GetCompletedExerciseIdsByCustomerWorkoutAsync( assignments.Where(a => !assignedCustomerFilter.HasValue || a.FKCustomerId == assignedCustomerFilter.Value) .Select(a => a.CustomerWorkoutId)); return plans.Select(workoutPlan => { assignmentsByPlan.TryGetValue(workoutPlan.WorkoutPlanId, out var planAssignments); planAssignments ??= new List(); var focusWorkoutId = assignedCustomerFilter.HasValue ? planAssignments.FirstOrDefault(a => a.FKCustomerId == assignedCustomerFilter.Value)?.CustomerWorkoutId : null; completedByWorkout.TryGetValue(focusWorkoutId ?? Guid.Empty, out var completedExerciseIds); 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 = MapWorkoutItemsWithExercises(workoutPlan.WorkoutItems, completedExerciseIds, onlyActive: assignedCustomerFilter.HasValue), AssignedCustomers = planAssignments .Select(cw => new CustomerWorkoutDto { CustomerWorkoutId = cw.CustomerWorkoutId, CustomerId = cw.FKCustomerId, CustomerName = cw.TenantCustomerLink?.DisplayName, WorkoutPlanId = cw.FKWorkoutPlanId, WorkoutPlanName = workoutPlan.WorkoutPlanName, IsActive = cw.IsActive, IsFollowing = cw.IsFollowing, AssignedDate = cw.AssignedDate, CompletedDate = cw.CompletedDate, Notes = cw.Notes }).ToList() }; }).ToList(); } public async Task> GetFollowedWorkoutPlansByCustomerIdAsync(Guid customerId) { var planIds = await _context.CustomerWorkouts .AsNoTracking() .Where(cw => cw.FKCustomerId == customerId && cw.IsActive && cw.IsFollowing) .OrderByDescending(cw => cw.AssignedDate) .Select(cw => cw.FKWorkoutPlanId) .Distinct() .ToListAsync(); return await BuildWorkoutPlanDetailsAsync(planIds, customerId); } // All of the customer's own AI-generated workout plans, whether currently followed or // not. Distinct from admin/client-authored templates (IsTemplate=true). public async Task> GetMyWorkoutPlansByCustomerIdAsync(Guid customerId) { var planIds = await _context.CustomerWorkouts .AsNoTracking() .Where(cw => cw.FKCustomerId == customerId && cw.IsActive) .OrderByDescending(cw => cw.AssignedDate) .Select(cw => cw.FKWorkoutPlanId) .Distinct() .ToListAsync(); var details = await BuildWorkoutPlanDetailsAsync(planIds, customerId); return details.Where(d => !d.IsTemplate).ToList(); } public async Task GetWorkoutPlanByIdForCustomerAsync(Guid workoutPlanId, Guid customerId) { var details = await BuildWorkoutPlanDetailsAsync(new[] { workoutPlanId }, customerId); return details.FirstOrDefault(); } public async Task GetActiveCustomerWorkoutAsync(Guid customerId, Guid workoutPlanId) { return await _context.CustomerWorkouts .FirstOrDefaultAsync(cw => cw.FKCustomerId == customerId && cw.FKWorkoutPlanId == workoutPlanId && cw.IsActive); } public async Task SetExerciseCompletionAsync(Guid customerWorkoutId, Guid workoutExerciseId, bool completed) { var existing = await _context.CustomerWorkoutExerciseCompletions .FirstOrDefaultAsync(c => c.FKCustomerWorkoutId == customerWorkoutId && c.FKWorkoutExerciseId == workoutExerciseId); if (completed) { if (existing == null) { await _context.CustomerWorkoutExerciseCompletions.AddAsync(new CustomerWorkoutExerciseCompletion { CustomerWorkoutExerciseCompletionId = Guid.NewGuid(), FKCustomerWorkoutId = customerWorkoutId, FKWorkoutExerciseId = workoutExerciseId, CompletedDate = DateTime.UtcNow }); } } else if (existing != null) { _context.CustomerWorkoutExerciseCompletions.Remove(existing); } } public async Task GetActiveWorkOutAsync(Guid? clientId = null, Guid? cusId = null) { if (clientId == null && cusId == null) throw new ArgumentException("Either clientId or cusId must be provided."); Guid? workoutPlanId; if (clientId != null) { // The plan may be a central template or this gym's own, so match on the // scalar id against plan ids gathered from both buckets. var clientPlanIds = await _context.WorkoutPlans .Where(wp => wp.FKClientId == clientId) .Select(wp => wp.WorkoutPlanId) .ToListAsync(); clientPlanIds.AddRange(await _central.WorkoutPlans .Where(wp => wp.FKClientId == clientId) .Select(wp => wp.WorkoutPlanId) .ToListAsync()); workoutPlanId = await _context.CustomerWorkouts .Where(cm => clientPlanIds.Contains(cm.FKWorkoutPlanId) && cm.IsActive && cm.FKCustomerId == cusId) .Select(cm => (Guid?)cm.FKWorkoutPlanId) .FirstOrDefaultAsync(); } else { workoutPlanId = await _context.CustomerWorkouts .Where(cm => cm.FKCustomerId == cusId && cm.IsActive) .Select(cm => (Guid?)cm.FKWorkoutPlanId) .FirstOrDefaultAsync(); } return workoutPlanId.HasValue ? await ResolveWorkoutPlanAsync(workoutPlanId.Value) : null; } public async Task FollowMealPlan(Guid customerId, Guid mealPlanId) { var currentFollow = await _context.CustomerMeals .FirstOrDefaultAsync(cm => cm.FKCustomerId == customerId && cm.IsFollowing); if (currentFollow != null && currentFollow.FKMealPlanId == mealPlanId) { return new { message = "Already following this meal plan" }; } if (currentFollow != null) { currentFollow.IsFollowing = false; _context.CustomerMeals.Update(currentFollow); } var existingFollow = await _context.CustomerMeals .FirstOrDefaultAsync(cm => cm.FKCustomerId == customerId && cm.FKMealPlanId == mealPlanId); if (existingFollow != null) { existingFollow.IsFollowing = true; existingFollow.IsActive = true; _context.CustomerMeals.Update(existingFollow); } else { var newFollow = new CustomerMeal { FKCustomerId = customerId, FKMealPlanId = mealPlanId, IsFollowing = true, IsActive = true }; _context.CustomerMeals.Add(newFollow); } return new { success = true, message = "Now following the selected meal plan" }; } public async Task FollowWorkoutPlan(Guid customerId, Guid workOutPlanId) { var currentFollow = await _context.CustomerWorkouts .FirstOrDefaultAsync(cm => cm.FKCustomerId == customerId && cm.IsFollowing); if (currentFollow != null && currentFollow.FKWorkoutPlanId == workOutPlanId) { return new { message = "Already following this workout plan" }; } if (currentFollow != null) { currentFollow.IsFollowing = false; _context.CustomerWorkouts.Update(currentFollow); } var existingFollow = await _context.CustomerWorkouts .FirstOrDefaultAsync(cm => cm.FKCustomerId == customerId && cm.FKWorkoutPlanId == workOutPlanId); if (existingFollow != null) { existingFollow.IsFollowing = true; existingFollow.IsActive = true; _context.CustomerWorkouts.Update(existingFollow); } else { var newFollow = new CustomerWorkout { FKCustomerId = customerId, FKWorkoutPlanId = workOutPlanId, IsFollowing = true, IsActive = true }; _context.CustomerWorkouts.Add(newFollow); } return new { success = true, message = "Now following the selected workout plan" }; } public async Task> GetAllClentShopsAsync() { return await _central.ClientBusiness .Where(x => x.Latitude != null && x.Longitude != null) .ToListAsync(); } public async Task GetClientBizDataAsync(Guid ClientBizId) { var biz = await _central.ClientBusiness .Include(c => c.Client) .Where(c => c.Bid == ClientBizId && c.IsActive == true) .FirstOrDefaultAsync(); if (biz == null) throw new NotFoundException($"{ClientBizId} business not found"); return biz; } public async Task GetCusHealthRec(Guid cusID) { CustomerHealth? customerHealth = await _central.CustomerHealth .Where(ch => ch.FKCusId == cusID && ch.IsActive == true) .FirstOrDefaultAsync(); if (customerHealth == null) return null; return customerHealth; } public async Task> GetCusProgressRec(Guid cusID) { var customerTargets = await _central.CustomerTargets .Where(ch => ch.FKCusId == cusID && ch.IsActive == true) .OrderByDescending(ch => ch.CreatedAt) .ToListAsync(); if (!customerTargets.Any()) throw new NotFoundException("No progress records found"); return customerTargets; } public async Task CreateCusHealthRecAsync(CustomerHealth customerHealth) { var cushealth = await _central.CustomerHealth .AddAsync(customerHealth); return customerHealth; } public async Task UpdateCusHealthRecAsync(CustomerHealth newCushealth) { _central.CustomerHealth.Update(newCushealth); return newCushealth; } public async Task CreateCustomerTargetAsync(CustomerTarget customerTarget) { await _central.CustomerTargets.AddAsync(customerTarget); return customerTarget; } public async Task GetLatestCustomerTargetAsync(Guid customerId) { return await _central.CustomerTargets .Where(ct => ct.FKCusId == customerId && ct.IsActive) .OrderByDescending(ct => ct.CreatedAt) .FirstOrDefaultAsync(); } public async Task> GetCustomerTargetsAsync(Guid customerId) { return await _central.CustomerTargets .Where(ct => ct.FKCusId == customerId && ct.IsActive) .OrderByDescending(ct => ct.CreatedAt) .ToListAsync(); } public async Task UpdateLatestCustomerTargetNutritionsAsync(Guid customerId, string predictedNutritionsJson) { var target = await _central.CustomerTargets .Where(ct => ct.FKCusId == customerId && ct.IsActive) .OrderByDescending(ct => ct.CreatedAt) .FirstOrDefaultAsync(); if (target == null) throw new NotFoundException("Customer target not found"); target.PredictedNutritionsJson = predictedNutritionsJson; target.IsChanged = true; _central.CustomerTargets.Update(target); return target; } public async Task>> GetCustomerProgressGroupedByGoalAsync(Guid cusID) { var progressList = await _central.CustomerTargets .Where(cp => cp.FKCusId == cusID && cp.IsActive) .OrderByDescending(cp => cp.CreatedAt) .ToListAsync(); var grouped = progressList .GroupBy(p => string.IsNullOrWhiteSpace(p.Goal) ? "Unknown" : p.Goal) .ToDictionary(g => g.Key, g => g.ToList()); return grouped; } public async Task UpdateWeeklyProgressAsync(Guid customerId, Guid customerTargetId, decimal weight, DateTime? recordedAt) { var target = await _central.CustomerTargets .FirstOrDefaultAsync(ct => ct.CustomerTargetId == customerTargetId && ct.FKCusId == customerId && ct.IsActive); if (target == null) throw new NotFoundException("Customer target not found"); var weeklyProgress = new CusWeightLossProgress { FKCusId = customerId, CustomerTargetId = customerTargetId, Weight = weight, RecordedAt = recordedAt ?? DateTime.UtcNow, IsActive = true }; await _central.CusWeightLossProgress.AddAsync(weeklyProgress); return weeklyProgress; } public async Task> GetWeeklyProgressForCustomerAsync(Guid customerId, Guid customerTargetId) { var targetExists = await _central.CustomerTargets .AnyAsync(ct => ct.CustomerTargetId == customerTargetId && ct.FKCusId == customerId && ct.IsActive); if (!targetExists) throw new NotFoundException("Customer target not found"); return await _central.CusWeightLossProgress .Where(wp => wp.FKCusId == customerId && wp.CustomerTargetId == customerTargetId && wp.IsActive) .OrderByDescending(wp => wp.RecordedAt) .ToListAsync(); } public async Task VerifyCustomer(Guid userId, string type, string identifire) { Customer customer = await _central.Customer .Where(c => c.FKUserId == userId && c.IsActive) .FirstOrDefaultAsync(); if (customer == null) return false; switch (type) { case "PrimaryPH": customer.IsPrimaryPHVerfied = true; customer.PrimaryPH = identifire; break; case "SecondaryPH": customer.IsSecondaryPHVerfied = true; customer.SecondaryPH = identifire; break; case "Email": customer.IsEmailVerfied = true; customer.Email = identifire; break; default: return false; } _central.Customer.Update(customer); return true; } public async Task IsverifiedCus(Guid cusId) { Customer? verifiedCus = await _central.Customer .Where(c => c.CusId == cusId && (c.IsEmailVerfied || c.IsPrimaryPHVerfied || c.IsSecondaryPHVerfied) && c.IsActive) .FirstOrDefaultAsync(); if(verifiedCus == null) return false; return true; } public async Task> GetCardsByCustomerIdAsync(Guid customerId, CancellationToken ct = default) { return await _context.CustomerCards .AsNoTracking() .Where(c => c.FK_CustomerId == customerId && c.IsActive) .ToListAsync(ct); } public async Task GetCardByIdAsync(Guid cardId, CancellationToken ct = default) { return await _context.CustomerCards .AsNoTracking() .FirstOrDefaultAsync(c => c.JobId == cardId, ct); } public async Task AddCardAsync(CustomerCards card, CancellationToken ct = default) { card.JobId = Guid.NewGuid(); _context.CustomerCards.Add(card); return card; } } }