first commit

This commit is contained in:
Dhananjaya99
2026-09-28 11:44:26 +05:30
parent 721743ae6b
commit bffc9b1686
424 changed files with 46474 additions and 0 deletions
+442
View File
@@ -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);
}
}