first commit
This commit is contained in:
@@ -0,0 +1,975 @@
|
||||
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.Meal
|
||||
{
|
||||
public class MealService : IMealService
|
||||
{
|
||||
private readonly IMealRepository _repository;
|
||||
private readonly UserRoleHelper _userRoleHelper;
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly IActivityLogService _activityLogService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
|
||||
public MealService(IMealRepository repository, UserRoleHelper userRoleHelper, IUnitOfWork uow, IActivityLogService activityLogService, IPushNotificationService pushNotificationService)
|
||||
{
|
||||
_repository = repository;
|
||||
_userRoleHelper = userRoleHelper;
|
||||
_uow = uow;
|
||||
_activityLogService = activityLogService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
}
|
||||
|
||||
// Meal Category operations
|
||||
public async Task<object> GetMealCategoryList(object payload, HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var categories = await _repository.GetMealCategoryListAsync();
|
||||
return categories;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> CreateMealCategory(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
var mealCategory = new MealCategory
|
||||
{
|
||||
Name = data["name"].GetString()!,
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealCategoryAsync(mealCategory);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(new { description = $"Meal category created with ID {mealCategory.MealCategoryId}" }, httpContext);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
return mealCategory;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Meal Plan operations
|
||||
public async Task<object> GetMealPlanList(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 mealPlans = await _repository.GetMealPlanListAsync(userType, clientId);
|
||||
return mealPlans;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> GetMealPlanById(object payload, HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
||||
|
||||
var mealPlan = await _repository.GetMealPlanByIdAsync(mealPlanId);
|
||||
|
||||
if (mealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found");
|
||||
|
||||
// Parse MealItems JSON strings to objects in nested meal items
|
||||
if (mealPlan.MealItems != null)
|
||||
{
|
||||
foreach (var item in mealPlan.MealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mealPlan;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception("An error occurred while fetching the meal plan details.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> GetMealPlanByCustomerId(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 mealPlans = await _repository.GetMealPlansByCustomerIdAsync(customerId);
|
||||
|
||||
|
||||
foreach (var mealPlan in mealPlans)
|
||||
{
|
||||
if (mealPlan.MealItems != null)
|
||||
{
|
||||
foreach (var item in mealPlan.MealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mealPlans;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new Exception("An error occurred while fetching meal plans for the customer.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> CreateMealPlan(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 newMealCategoryId = Guid.Parse(data["mealCategoryId"].GetString()!);
|
||||
if (!await _repository.MealCategoryExistsAsync(newMealCategoryId))
|
||||
throw new NotFoundException("Meal category not found.");
|
||||
|
||||
// Create meal plan
|
||||
var mealPlan = new MealPlan
|
||||
{
|
||||
Name = data["name"].GetString()!,
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
|
||||
IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(),
|
||||
FKMealCategoryId = newMealCategoryId,
|
||||
FKClientId = clientId,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealPlanAsync(mealPlan);
|
||||
|
||||
// Handle customer assignments if provided
|
||||
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 customerMeal = new CustomerMeal
|
||||
{
|
||||
FKCustomerId = customerId,
|
||||
FKMealPlanId = mealPlan.MealPlanId,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
IsActive = true
|
||||
};
|
||||
await _repository.AssignMealToCustomerAsync(customerMeal);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Backward compatibility: single customerId field
|
||||
else if (data.ContainsKey("customerId") && !string.IsNullOrEmpty(data["customerId"].GetString()))
|
||||
{
|
||||
var customerId = Guid.Parse(data["customerId"].GetString()!);
|
||||
var customerMeal = new CustomerMeal
|
||||
{
|
||||
FKCustomerId = customerId,
|
||||
FKMealPlanId = mealPlan.MealPlanId,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
IsActive = true
|
||||
};
|
||||
await _repository.AssignMealToCustomerAsync(customerMeal);
|
||||
}
|
||||
|
||||
// Handle meal items if provided
|
||||
if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
var mealItemsArray = data["mealItems"].EnumerateArray();
|
||||
foreach (var mealItemData in mealItemsArray)
|
||||
{
|
||||
// Only create meal if name is provided
|
||||
if (mealItemData.TryGetProperty("name", out var nameElement) &&
|
||||
!string.IsNullOrEmpty(nameElement.GetString()))
|
||||
{
|
||||
// Parse nested mealItems JSON
|
||||
string? mealItemsJson = null;
|
||||
if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement))
|
||||
{
|
||||
if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement);
|
||||
}
|
||||
else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
mealItemsJson = mealItemsElement.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
var meal = new Models.Meal
|
||||
{
|
||||
Name = nameElement.GetString()!,
|
||||
Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null,
|
||||
Calories = TryParseDecimal(mealItemData, "calories"),
|
||||
Protein = TryParseDecimal(mealItemData, "protein"),
|
||||
Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"),
|
||||
Fats = TryParseDecimal(mealItemData, "fats"),
|
||||
ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null,
|
||||
MealItems = mealItemsJson,
|
||||
PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null,
|
||||
FKMealPlanId = mealPlan.MealPlanId,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealAsync(meal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Meal plan created with ID {mealPlan.MealPlanId}" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
// Fetch and return the created meal plan with parsed JSON
|
||||
var createdMealPlan = await _repository.GetMealPlanByIdAsync(mealPlan.MealPlanId);
|
||||
|
||||
if (createdMealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlan.MealPlanId} not found after creation");
|
||||
|
||||
// Parse MealItems JSON strings to objects in nested meal items
|
||||
if (createdMealPlan.MealItems != null)
|
||||
{
|
||||
foreach (var item in createdMealPlan.MealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdMealPlan;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Plans created via the AI chat flow aren't given a category by the customer — the
|
||||
// mobile app used to guess one by fetching the category list and taking the first
|
||||
// entry, which was both an extra round-trip and an arbitrary pick. Categories are a
|
||||
// global taxonomy (MealCategory has no FKClientId), so a single well-known "AI
|
||||
// Generated" category is get-or-created here and reused for every gym's AI plans.
|
||||
private const string AiGeneratedCategoryName = "AI Generated";
|
||||
|
||||
private async Task<Guid> GetOrCreateAiMealCategoryIdAsync()
|
||||
{
|
||||
var categories = await _repository.GetMealCategoryListAsync();
|
||||
var existing = categories.FirstOrDefault(c =>
|
||||
string.Equals(c.Name, AiGeneratedCategoryName, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing != null)
|
||||
return existing.MealCategoryId;
|
||||
|
||||
var created = await _repository.CreateMealCategoryAsync(new MealCategory
|
||||
{
|
||||
MealCategoryId = Guid.NewGuid(),
|
||||
Name = AiGeneratedCategoryName,
|
||||
Description = "Plans generated by the AI meal planner",
|
||||
IsActive = true,
|
||||
CreateDate = DateTime.UtcNow
|
||||
});
|
||||
return created.MealCategoryId;
|
||||
}
|
||||
|
||||
public async Task<object> CreateCustomerMealPlan(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 mealCategoryId = data.ContainsKey("mealCategoryId") && !string.IsNullOrEmpty(data["mealCategoryId"].GetString())
|
||||
? Guid.Parse(data["mealCategoryId"].GetString()!)
|
||||
: await GetOrCreateAiMealCategoryIdAsync();
|
||||
|
||||
var mealPlan = new MealPlan
|
||||
{
|
||||
Name = data["name"].GetString()!,
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
|
||||
IsTemplate = data.ContainsKey("isTemplate") && data["isTemplate"].GetBoolean(),
|
||||
FKMealCategoryId = mealCategoryId,
|
||||
FKClientId = clientId,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealPlanAsync(mealPlan);
|
||||
|
||||
if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
var mealItemsArray = data["mealItems"].EnumerateArray();
|
||||
foreach (var mealItemData in mealItemsArray)
|
||||
{
|
||||
if (mealItemData.TryGetProperty("name", out var nameElement) &&
|
||||
!string.IsNullOrEmpty(nameElement.GetString()))
|
||||
{
|
||||
string? mealItemsJson = null;
|
||||
if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement))
|
||||
{
|
||||
if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement);
|
||||
}
|
||||
else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
mealItemsJson = mealItemsElement.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
var meal = new Models.Meal
|
||||
{
|
||||
Name = nameElement.GetString()!,
|
||||
Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null,
|
||||
Calories = TryParseDecimal(mealItemData, "calories"),
|
||||
Protein = TryParseDecimal(mealItemData, "protein"),
|
||||
Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"),
|
||||
Fats = TryParseDecimal(mealItemData, "fats"),
|
||||
ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null,
|
||||
MealItems = mealItemsJson,
|
||||
PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null,
|
||||
FKMealPlanId = mealPlan.MealPlanId,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealAsync(meal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var customerMeal = new CustomerMeal
|
||||
{
|
||||
FKCustomerId = customerId,
|
||||
FKMealPlanId = mealPlan.MealPlanId,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
IsFollowing = true
|
||||
};
|
||||
await _repository.AssignMealToCustomerAsync(customerMeal);
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Customer meal plan created with ID {mealPlan.MealPlanId}" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
var createdMealPlan = await _repository.GetMealPlanByIdAsync(mealPlan.MealPlanId);
|
||||
|
||||
if (createdMealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlan.MealPlanId} not found after creation");
|
||||
|
||||
if (createdMealPlan.MealItems != null)
|
||||
{
|
||||
foreach (var item in createdMealPlan.MealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdMealPlan;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Meal Item operations
|
||||
public async Task<object> GetMealItemsByMealPlanId(object payload, HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
||||
|
||||
var mealItems = await _repository.GetMealsByMealPlanIdAsync(mealPlanId);
|
||||
|
||||
// Parse MealItems JSON strings to objects
|
||||
foreach (var item in mealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mealItems;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> CreateMealItem(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
|
||||
// Parse mealItems array and serialize to JSON string
|
||||
string? mealItemsJson = null;
|
||||
if (data.ContainsKey("mealItems"))
|
||||
{
|
||||
var mealItemsElement = data["mealItems"];
|
||||
if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement);
|
||||
}
|
||||
else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
mealItemsJson = mealItemsElement.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
var mealItem = new Models.Meal
|
||||
{
|
||||
Name = data["name"].GetString()!,
|
||||
Description = data.ContainsKey("description") ? data["description"].GetString() : null,
|
||||
Calories = data.ContainsKey("calories") && !string.IsNullOrEmpty(data["calories"].GetString())
|
||||
? decimal.Parse(data["calories"].GetString()!)
|
||||
: (decimal?)null,
|
||||
Protein = data.ContainsKey("protein") && !string.IsNullOrEmpty(data["protein"].GetString())
|
||||
? decimal.Parse(data["protein"].GetString()!)
|
||||
: (decimal?)null,
|
||||
Carbohydrates = data.ContainsKey("carbohydrates") && !string.IsNullOrEmpty(data["carbohydrates"].GetString())
|
||||
? decimal.Parse(data["carbohydrates"].GetString()!)
|
||||
: (decimal?)null,
|
||||
Fats = data.ContainsKey("fats") && !string.IsNullOrEmpty(data["fats"].GetString())
|
||||
? decimal.Parse(data["fats"].GetString()!)
|
||||
: (decimal?)null,
|
||||
ServingSize = data.ContainsKey("servingSize") ? data["servingSize"].GetString() : null,
|
||||
MealItems = mealItemsJson,
|
||||
PreparationNotes = data.ContainsKey("preparationNotes") ? data["preparationNotes"].GetString() : null,
|
||||
FKMealPlanId = Guid.Parse(data["mealPlanId"].GetString()!),
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealAsync(mealItem);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
// Log activity - non-blocking, failures won't crash the operation
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(new { description = $"Meal item created with ID {mealItem.MealId}" }, httpContext);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
// Parse mealItems JSON string to object before returning
|
||||
object? parsedMealItems = mealItem.MealItems;
|
||||
if (!string.IsNullOrEmpty(mealItem.MealItems))
|
||||
{
|
||||
try
|
||||
{
|
||||
parsedMealItems = System.Text.Json.JsonSerializer.Deserialize<object>(mealItem.MealItems);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
mealItem.MealId,
|
||||
mealItem.Name,
|
||||
mealItem.Description,
|
||||
mealItem.Calories,
|
||||
mealItem.Protein,
|
||||
mealItem.Carbohydrates,
|
||||
mealItem.Fats,
|
||||
mealItem.ServingSize,
|
||||
MealItems = parsedMealItems,
|
||||
mealItem.PreparationNotes,
|
||||
FKMealPlanId = mealItem.FKMealPlanId,
|
||||
mealItem.IsActive,
|
||||
mealItem.CreateDate
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Meal Plan
|
||||
public async Task<object> UpdateMealPlan(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
||||
|
||||
var existingMealPlan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId);
|
||||
if (existingMealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found");
|
||||
|
||||
// Update meal plan properties
|
||||
if (data.ContainsKey("name") && !string.IsNullOrEmpty(data["name"].GetString()))
|
||||
existingMealPlan.Name = data["name"].GetString()!;
|
||||
|
||||
if (data.ContainsKey("description"))
|
||||
existingMealPlan.Description = data["description"].GetString();
|
||||
|
||||
if (data.ContainsKey("isTemplate"))
|
||||
existingMealPlan.IsTemplate = data["isTemplate"].GetBoolean();
|
||||
|
||||
if (data.ContainsKey("mealCategoryId"))
|
||||
existingMealPlan.FKMealCategoryId = Guid.Parse(data["mealCategoryId"].GetString()!);
|
||||
|
||||
await _repository.UpdateMealPlanAsync(existingMealPlan);
|
||||
|
||||
// Handle meal items update if provided
|
||||
if (data.ContainsKey("mealItems") && data["mealItems"].ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
// Get existing meal items for this plan
|
||||
var existingMeals = await _repository.GetMealsByMealPlanIdAsync(mealPlanId);
|
||||
|
||||
// Delete all existing meals (clean slate approach)
|
||||
foreach (var existingMeal in existingMeals)
|
||||
{
|
||||
await _repository.DeleteMealAsync(existingMeal.MealId);
|
||||
}
|
||||
|
||||
// Create new meals from payload
|
||||
var mealItemsArray = data["mealItems"].EnumerateArray();
|
||||
foreach (var mealItemData in mealItemsArray)
|
||||
{
|
||||
// Only create meal if name is provided
|
||||
if (mealItemData.TryGetProperty("name", out var nameElement) &&
|
||||
!string.IsNullOrEmpty(nameElement.GetString()))
|
||||
{
|
||||
string? mealItemsJson = null;
|
||||
if (mealItemData.TryGetProperty("mealItems", out var mealItemsElement))
|
||||
{
|
||||
if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
mealItemsJson = System.Text.Json.JsonSerializer.Serialize(mealItemsElement);
|
||||
}
|
||||
else if (mealItemsElement.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
mealItemsJson = mealItemsElement.GetString();
|
||||
}
|
||||
}
|
||||
|
||||
var meal = new Models.Meal
|
||||
{
|
||||
Name = nameElement.GetString()!,
|
||||
Description = mealItemData.TryGetProperty("description", out var desc) ? desc.GetString() : null,
|
||||
Calories = TryParseDecimal(mealItemData, "calories"),
|
||||
Protein = TryParseDecimal(mealItemData, "protein"),
|
||||
Carbohydrates = TryParseDecimal(mealItemData, "carbohydrates"),
|
||||
Fats = TryParseDecimal(mealItemData, "fats"),
|
||||
ServingSize = mealItemData.TryGetProperty("servingSize", out var serving) ? serving.GetString() : null,
|
||||
MealItems = mealItemsJson,
|
||||
PreparationNotes = mealItemData.TryGetProperty("preparationNotes", out var notes) ? notes.GetString() : null,
|
||||
FKMealPlanId = mealPlanId,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.CreateMealAsync(meal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Meal plan updated with ID {mealPlanId}" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
var updatedMealPlan = await _repository.GetMealPlanByIdAsync(mealPlanId);
|
||||
|
||||
if (updatedMealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found after update");
|
||||
|
||||
// Parse MealItems JSON strings to objects in nested meal items
|
||||
if (updatedMealPlan.MealItems != null)
|
||||
{
|
||||
foreach (var item in updatedMealPlan.MealItems)
|
||||
{
|
||||
var itemAsString = item.MealItems as string;
|
||||
if (!string.IsNullOrEmpty(itemAsString))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.MealItems = System.Text.Json.JsonSerializer.Deserialize<object>(itemAsString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep as string if parsing fails
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMealPlan;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Meal Plan
|
||||
public async Task<object> DeleteMealPlan(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
||||
|
||||
var existingMealPlan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId);
|
||||
if (existingMealPlan == null)
|
||||
throw new KeyNotFoundException($"Meal plan with ID {mealPlanId} not found");
|
||||
|
||||
// Delete all associated meal items first (foreign key constraint)
|
||||
var existingMeals = await _repository.GetMealsByMealPlanIdAsync(mealPlanId);
|
||||
foreach (var meal in existingMeals)
|
||||
{
|
||||
await _repository.DeleteMealAsync(meal.MealId);
|
||||
}
|
||||
|
||||
// Delete the meal plan
|
||||
await _repository.DeleteMealPlanAsync(mealPlanId);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Meal plan deleted with ID {mealPlanId}" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
Success = true,
|
||||
Message = $"Meal plan with ID {mealPlanId} has been deleted successfully"
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method
|
||||
private decimal? TryParseDecimal(System.Text.Json.JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.TryGetProperty(propertyName, out var prop))
|
||||
{
|
||||
if (prop.ValueKind == System.Text.Json.JsonValueKind.Number)
|
||||
{
|
||||
return prop.GetDecimal();
|
||||
}
|
||||
else if (prop.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
var strValue = prop.GetString();
|
||||
if (!string.IsNullOrEmpty(strValue) && decimal.TryParse(strValue, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper for JsonElement
|
||||
private decimal? TryParseDecimal(Dictionary<string, System.Text.Json.JsonElement> data, string key)
|
||||
{
|
||||
if (!data.ContainsKey(key))
|
||||
return null;
|
||||
|
||||
var element = data[key];
|
||||
if (element.ValueKind == System.Text.Json.JsonValueKind.Number)
|
||||
return element.GetDecimal();
|
||||
|
||||
if (element.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
{
|
||||
var strValue = element.GetString();
|
||||
if (!string.IsNullOrEmpty(strValue) && decimal.TryParse(strValue, out var result))
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Customer Meal Assignment operations
|
||||
public async Task<object> AssignMealToCustomer(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid mealPlanId = Guid.Parse(data["mealPlanId"].GetString()!);
|
||||
Guid customerId = Guid.Parse(data["customerId"].GetString()!);
|
||||
|
||||
var customerMeal = new CustomerMeal
|
||||
{
|
||||
FKCustomerId = customerId,
|
||||
FKMealPlanId = mealPlanId,
|
||||
AssignedDate = DateTime.UtcNow,
|
||||
Notes = data.ContainsKey("notes") ? data["notes"].GetString() : null,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
await _repository.AssignMealToCustomerAsync(customerMeal);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Meal plan {mealPlanId} assigned to customer {customerId}" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var plan = await _repository.GetMealPlanEntityByIdAsync(mealPlanId);
|
||||
var planName = plan?.Name ?? "a meal plan";
|
||||
await _pushNotificationService.NotifyCustomerAsync(
|
||||
customerId,
|
||||
"New meal plan assigned",
|
||||
$"Your coach assigned you \"{planName}\". Tap to view.",
|
||||
"MealPlanAssigned",
|
||||
"MealPlanSuggestionDetail",
|
||||
new { mealPlanId }
|
||||
);
|
||||
}
|
||||
catch (Exception notifyEx)
|
||||
{
|
||||
Console.WriteLine($"Push notification failed (non-critical): {notifyEx.Message}");
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
customerMeal.CustomerMealId,
|
||||
customerMeal.FKCustomerId,
|
||||
customerMeal.FKMealPlanId,
|
||||
customerMeal.AssignedDate,
|
||||
customerMeal.Notes,
|
||||
customerMeal.IsActive
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> UnassignMealFromCustomer(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
Guid customerMealId = Guid.Parse(data["customerMealId"].GetString()!);
|
||||
|
||||
var result = await _repository.UnassignMealFromCustomerAsync(customerMealId);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
throw new KeyNotFoundException($"Customer meal assignment with ID {customerMealId} not found");
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _activityLogService.LogActivity(
|
||||
new { description = $"Meal assignment {customerMealId} removed" },
|
||||
httpContext
|
||||
);
|
||||
}
|
||||
catch (Exception logEx)
|
||||
{
|
||||
Console.WriteLine($"Activity log failed (non-critical): {logEx.Message}");
|
||||
}
|
||||
|
||||
return new { success = true, message = "Meal unassigned from customer successfully" };
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user