first commit
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
using Fitness_core_BE.Models;
|
||||
using Fitness_core_BE.Interfaces;
|
||||
using Fitness_core_BE.DTOs.Meal;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Fitness_core_BE.Repos
|
||||
{
|
||||
public class MealRepository : IMealRepository
|
||||
{
|
||||
// Client-authored (non-template) meal plans + all CustomerMeal assignment rows.
|
||||
private readonly TenantDbContext _dbContext;
|
||||
// Admin-authored templates (MealCategories, and MealPlans/Meal where IsTemplate=true).
|
||||
private readonly CentralDbContext _central;
|
||||
|
||||
public MealRepository(TenantDbContext dbContext, CentralDbContext central)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
_central = central;
|
||||
}
|
||||
|
||||
// Meal Category operations -- global lookup, admin-owned, always central.
|
||||
public async Task<List<MealCategoryDto>> GetMealCategoryListAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _central.MealCategories
|
||||
.AsNoTracking()
|
||||
.Where(mc => mc.IsActive)
|
||||
.OrderBy(mc => mc.Name)
|
||||
.Select(mc => new MealCategoryDto
|
||||
{
|
||||
MealCategoryId = mc.MealCategoryId,
|
||||
Name = mc.Name,
|
||||
Description = mc.Description,
|
||||
IsActive = mc.IsActive,
|
||||
CreateDate = mc.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> MealCategoryExistsAsync(Guid mealCategoryId, CancellationToken ct = default)
|
||||
{
|
||||
return _central.MealCategories
|
||||
.AsNoTracking()
|
||||
.AnyAsync(mc => mc.MealCategoryId == mealCategoryId && mc.IsActive, ct);
|
||||
}
|
||||
|
||||
public async Task<MealCategory> CreateMealCategoryAsync(MealCategory mealCategory, CancellationToken ct = default)
|
||||
{
|
||||
await _central.MealCategories.AddAsync(mealCategory, ct);
|
||||
return mealCategory;
|
||||
}
|
||||
|
||||
// Meal Plan operations
|
||||
public async Task<List<MealPlanDto>> GetMealPlanListAsync(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) -- these are two
|
||||
// disjoint sets post-split, never merged in one query.
|
||||
if (userType == "Customer")
|
||||
{
|
||||
return await _central.MealPlans
|
||||
.AsNoTracking()
|
||||
.Include(mp => mp.MealCategory)
|
||||
.Where(mp => mp.IsActive && mp.IsTemplate)
|
||||
.OrderByDescending(mp => mp.CreateDate)
|
||||
.Select(mp => new MealPlanDto
|
||||
{
|
||||
MealPlanId = mp.MealPlanId,
|
||||
Name = mp.Name,
|
||||
Description = mp.Description,
|
||||
IsTemplate = mp.IsTemplate,
|
||||
MealCategoryId = mp.FKMealCategoryId,
|
||||
MealCategoryName = mp.MealCategory != null ? mp.MealCategory.Name : "",
|
||||
ClientId = mp.FKClientId,
|
||||
ClientName = null,
|
||||
IsActive = mp.IsActive,
|
||||
CreateDate = mp.CreateDate,
|
||||
AssignedCustomers = new List<CustomerMealDto>()
|
||||
})
|
||||
.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.MealCategories
|
||||
.AsNoTracking()
|
||||
.ToDictionaryAsync(mc => mc.MealCategoryId, mc => mc.Name, ct);
|
||||
|
||||
var tenantPlans = await _dbContext.MealPlans
|
||||
.AsNoTracking()
|
||||
.Include(mp => mp.CustomerMeals.Where(cm => cm.IsActive))
|
||||
.ThenInclude(cm => cm.TenantCustomerLink)
|
||||
.Where(mp => mp.IsActive && !mp.IsTemplate && mp.FKClientId == clientId)
|
||||
.OrderByDescending(mp => mp.CreateDate)
|
||||
.Select(mp => new MealPlanDto
|
||||
{
|
||||
MealPlanId = mp.MealPlanId,
|
||||
Name = mp.Name,
|
||||
Description = mp.Description,
|
||||
IsTemplate = mp.IsTemplate,
|
||||
MealCategoryId = mp.FKMealCategoryId,
|
||||
MealCategoryName = "",
|
||||
ClientId = mp.FKClientId,
|
||||
ClientName = null,
|
||||
IsActive = mp.IsActive,
|
||||
CreateDate = mp.CreateDate,
|
||||
AssignedCustomers = mp.CustomerMeals
|
||||
.Where(cm => cm.IsActive)
|
||||
.Select(cm => new CustomerMealDto
|
||||
{
|
||||
CustomerMealId = cm.CustomerMealId,
|
||||
CustomerId = cm.FKCustomerId,
|
||||
CustomerName = cm.TenantCustomerLink != null ? cm.TenantCustomerLink.DisplayName : null,
|
||||
MealPlanId = cm.FKMealPlanId,
|
||||
IsActive = cm.IsActive,
|
||||
IsFollowing = cm.IsFollowing,
|
||||
AssignedDate = cm.AssignedDate,
|
||||
CompletedDate = cm.CompletedDate,
|
||||
Notes = cm.Notes
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var plan in tenantPlans)
|
||||
{
|
||||
plan.MealCategoryName =
|
||||
categoryNames.TryGetValue(plan.MealCategoryId, out var name) ? name : "";
|
||||
}
|
||||
|
||||
return tenantPlans;
|
||||
}
|
||||
|
||||
public async Task<MealPlanDetailDto?> GetMealPlanByIdAsync(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.MealPlans
|
||||
.AsNoTracking()
|
||||
.Include(mp => mp.MealCategory)
|
||||
.Include(mp => mp.MealItems)
|
||||
.FirstOrDefaultAsync(mp => mp.MealPlanId == id && mp.IsActive, ct);
|
||||
if (centralPlan != null)
|
||||
return MapDetail(centralPlan, Array.Empty<CustomerMeal>());
|
||||
|
||||
var tenantPlan = await _dbContext.MealPlans
|
||||
.AsNoTracking()
|
||||
.Include(mp => mp.CustomerMeals.Where(cm => cm.IsActive))
|
||||
.ThenInclude(cm => cm.TenantCustomerLink)
|
||||
.Include(mp => mp.MealItems)
|
||||
.FirstOrDefaultAsync(mp => mp.MealPlanId == id && mp.IsActive, ct);
|
||||
if (tenantPlan == null)
|
||||
return null;
|
||||
|
||||
var detail = MapDetail(tenantPlan, tenantPlan.CustomerMeals.Where(cm => cm.IsActive).ToList());
|
||||
// Category is central-only, so it can't be Include()d from the tenant context.
|
||||
detail.MealCategoryName = await _central.MealCategories
|
||||
.AsNoTracking()
|
||||
.Where(mc => mc.MealCategoryId == tenantPlan.FKMealCategoryId)
|
||||
.Select(mc => mc.Name)
|
||||
.FirstOrDefaultAsync(ct) ?? "";
|
||||
return detail;
|
||||
}
|
||||
|
||||
private static MealPlanDetailDto MapDetail(MealPlan mealPlan, IReadOnlyCollection<CustomerMeal> assigned)
|
||||
{
|
||||
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();
|
||||
|
||||
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 = assigned
|
||||
.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)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<MealPlanDetailDto>> GetMealPlansByCustomerIdAsync(Guid customerId, CancellationToken ct = default)
|
||||
{
|
||||
// CustomerMeal (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.CustomerMeals
|
||||
.AsNoTracking()
|
||||
.Where(cm => cm.FKCustomerId == customerId && cm.IsActive)
|
||||
.OrderByDescending(cm => cm.AssignedDate)
|
||||
.Select(cm => cm.FKMealPlanId)
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
|
||||
var result = new List<MealPlanDetailDto>();
|
||||
foreach (var mealPlanId in follows)
|
||||
{
|
||||
var detail = await GetMealPlanByIdAsync(mealPlanId, ct);
|
||||
if (detail != null && detail.IsActive)
|
||||
result.Add(detail);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<MealPlan?> GetMealPlanEntityByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.MealPlans
|
||||
.FirstOrDefaultAsync(mp => mp.MealPlanId == id && mp.IsActive, ct);
|
||||
}
|
||||
|
||||
public async Task<MealPlan> CreateMealPlanAsync(MealPlan mealPlan, CancellationToken ct = default)
|
||||
{
|
||||
await _dbContext.MealPlans.AddAsync(mealPlan, ct);
|
||||
return mealPlan;
|
||||
}
|
||||
|
||||
public async Task<MealPlan> UpdateMealPlanAsync(MealPlan mealPlan, CancellationToken ct = default)
|
||||
{
|
||||
_dbContext.MealPlans.Update(mealPlan);
|
||||
return mealPlan;
|
||||
}
|
||||
|
||||
public async Task DeleteMealPlanAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var mealPlan = await _dbContext.MealPlans.FindAsync(new object[] { id }, ct);
|
||||
if (mealPlan != null)
|
||||
{
|
||||
// Soft delete by setting IsActive to false
|
||||
mealPlan.IsActive = false;
|
||||
_dbContext.MealPlans.Update(mealPlan);
|
||||
}
|
||||
}
|
||||
|
||||
// Meal Item operations
|
||||
public async Task<List<MealDto>> GetMealsByMealPlanIdAsync(Guid mealPlanId, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Meal
|
||||
.AsNoTracking()
|
||||
.Where(mi => mi.FKMealPlanId == mealPlanId && mi.IsActive)
|
||||
.OrderBy(mi => mi.CreateDate)
|
||||
.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
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Meal?> GetMealByIdAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Meal
|
||||
.FirstOrDefaultAsync(mi => mi.MealId == id && mi.IsActive, ct);
|
||||
}
|
||||
|
||||
public async Task<Meal> CreateMealAsync(Meal mealItem, CancellationToken ct = default)
|
||||
{
|
||||
await _dbContext.Meal.AddAsync(mealItem, ct);
|
||||
return mealItem;
|
||||
}
|
||||
|
||||
public async Task<Meal> UpdateMealAsync(Meal mealItem, CancellationToken ct = default)
|
||||
{
|
||||
_dbContext.Meal.Update(mealItem);
|
||||
return mealItem;
|
||||
}
|
||||
|
||||
public async Task DeleteMealAsync(Guid id, CancellationToken ct = default)
|
||||
{
|
||||
var meal = await _dbContext.Meal.FindAsync(new object[] { id }, ct);
|
||||
if (meal != null)
|
||||
{
|
||||
// Soft delete by setting IsActive to false
|
||||
meal.IsActive = false;
|
||||
_dbContext.Meal.Update(meal);
|
||||
}
|
||||
}
|
||||
|
||||
// Customer Meal Assignment operations
|
||||
public async Task<CustomerMeal> AssignMealToCustomerAsync(CustomerMeal customerMeal, CancellationToken ct = default)
|
||||
{
|
||||
// Mirrors WorkoutRepository.AssignWorkoutToCustomerAsync: a coach assigning a plan
|
||||
// must become the customer's "Followed" plan, the same as self-following one from
|
||||
// Suggestions — otherwise an admin-suggested plan the customer already follows keeps
|
||||
// showing instead of the one the client just assigned.
|
||||
var currentFollow = await _dbContext.CustomerMeals
|
||||
.FirstOrDefaultAsync(cm => cm.FKCustomerId == customerMeal.FKCustomerId && cm.IsFollowing, ct);
|
||||
|
||||
if (currentFollow != null && currentFollow.FKMealPlanId != customerMeal.FKMealPlanId)
|
||||
{
|
||||
currentFollow.IsFollowing = false;
|
||||
_dbContext.CustomerMeals.Update(currentFollow);
|
||||
}
|
||||
|
||||
var existing = await _dbContext.CustomerMeals
|
||||
.FirstOrDefaultAsync(cm => cm.FKCustomerId == customerMeal.FKCustomerId && cm.FKMealPlanId == customerMeal.FKMealPlanId, ct);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsFollowing = true;
|
||||
existing.IsActive = true;
|
||||
existing.AssignedDate = customerMeal.AssignedDate;
|
||||
existing.Notes = customerMeal.Notes;
|
||||
_dbContext.CustomerMeals.Update(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
customerMeal.IsFollowing = true;
|
||||
await _dbContext.CustomerMeals.AddAsync(customerMeal, ct);
|
||||
return customerMeal;
|
||||
}
|
||||
|
||||
public async Task<bool> UnassignMealFromCustomerAsync(Guid customerMealId, CancellationToken ct = default)
|
||||
{
|
||||
var customerMeal = await _dbContext.CustomerMeals.FindAsync(new object[] { customerMealId }, ct);
|
||||
|
||||
if (customerMeal == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Soft delete by setting IsActive to false
|
||||
customerMeal.IsActive = false;
|
||||
_dbContext.CustomerMeals.Update(customerMeal);
|
||||
|
||||
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>> ResolveMealPlanNamesAsync(
|
||||
IReadOnlyCollection<Guid> planIds, CancellationToken ct)
|
||||
{
|
||||
if (planIds.Count == 0)
|
||||
return new Dictionary<Guid, string>();
|
||||
|
||||
var names = await _dbContext.MealPlans
|
||||
.AsNoTracking()
|
||||
.Where(mp => planIds.Contains(mp.MealPlanId))
|
||||
.ToDictionaryAsync(mp => mp.MealPlanId, mp => mp.Name, ct);
|
||||
|
||||
var missing = planIds.Where(id => !names.ContainsKey(id)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
var centralNames = await _central.MealPlans
|
||||
.AsNoTracking()
|
||||
.Where(mp => missing.Contains(mp.MealPlanId))
|
||||
.ToDictionaryAsync(mp => mp.MealPlanId, mp => mp.Name, ct);
|
||||
foreach (var pair in centralNames)
|
||||
names[pair.Key] = pair.Value;
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
private async Task<List<CustomerMealDto>> BuildCustomerMealDtosAsync(
|
||||
IQueryable<CustomerMeal> query, CancellationToken ct)
|
||||
{
|
||||
var rows = await query
|
||||
.AsNoTracking()
|
||||
.Include(cm => cm.TenantCustomerLink)
|
||||
.OrderByDescending(cm => cm.AssignedDate)
|
||||
.Select(cm => new CustomerMealDto
|
||||
{
|
||||
CustomerMealId = cm.CustomerMealId,
|
||||
CustomerId = cm.FKCustomerId,
|
||||
CustomerName = cm.TenantCustomerLink != null ? cm.TenantCustomerLink.DisplayName : null,
|
||||
MealPlanId = cm.FKMealPlanId,
|
||||
MealPlanName = null,
|
||||
IsActive = cm.IsActive,
|
||||
IsFollowing = cm.IsFollowing,
|
||||
AssignedDate = cm.AssignedDate,
|
||||
CompletedDate = cm.CompletedDate,
|
||||
Notes = cm.Notes
|
||||
})
|
||||
.ToListAsync(ct);
|
||||
|
||||
var names = await ResolveMealPlanNamesAsync(
|
||||
rows.Select(r => r.MealPlanId).Distinct().ToList(), ct);
|
||||
|
||||
foreach (var row in rows)
|
||||
row.MealPlanName = names.TryGetValue(row.MealPlanId, out var name) ? name : null;
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
public Task<List<CustomerMealDto>> GetCustomerMealsByPlanIdAsync(Guid mealPlanId, CancellationToken ct = default)
|
||||
=> BuildCustomerMealDtosAsync(
|
||||
_dbContext.CustomerMeals.Where(cm => cm.FKMealPlanId == mealPlanId && cm.IsActive), ct);
|
||||
|
||||
public Task<List<CustomerMealDto>> GetCustomerMealsByCustomerIdAsync(Guid customerId, CancellationToken ct = default)
|
||||
=> BuildCustomerMealDtosAsync(
|
||||
_dbContext.CustomerMeals.Where(cm => cm.FKCustomerId == customerId && cm.IsActive), ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user