From a599b17a5047c2bccf764822eb2eac09b5a9a080 Mon Sep 17 00:00:00 2001 From: Kevin Tran Date: Tue, 10 Mar 2026 01:25:40 -0500 Subject: [PATCH] Revert "Merge pull request #118 from Culinary-Command/revert-113-CC-111/recipe-logic" This reverts commit a77d268e289442d3ba56d0834af88ed41641d16e, reversing changes made to e6f164aeb7e994ed06cede47480dfee29e328f9f. --- .../Pages/Assignments/AdminAssignTask.razor | 12 +- .../Components/Pages/EmployeeView.razor | 64 + .../Components/Pages/Recipes/Create.razor | 31 - .../Components/Pages/Recipes/Edit.razor | 33 - .../Components/Pages/Recipes/Index.razor | 5 - .../Components/Pages/Recipes/RecipeForm.razor | 234 ---- .../Components/Pages/Recipes/RecipeList.razor | 167 --- .../Components/Pages/Recipes/RecipeView.razor | 133 -- .../Pages/Recipes/RecipeView.razor.css | 10 - .../LocationSettings/ConfigureLocation.razor | 152 +++ CulinaryCommandApp/Data/AppDbContext.cs | 118 +- .../Data/Entities/Ingredient.cs | 26 - CulinaryCommandApp/Data/Entities/Location.cs | 8 +- .../Data/Entities/MeasurementUnit.cs | 24 - CulinaryCommandApp/Data/Entities/Recipe.cs | 37 - .../Data/Entities/RecipeIngredient.cs | 37 - .../Data/Entities/RecipeStep.cs | 25 - CulinaryCommandApp/Data/Entities/Tasks.cs | 5 +- .../Data/Models/MeasurementUnitViewModel.cs | 15 - .../Inventory/DTOs/CreateIngredientDTO.cs | 2 +- .../Inventory/DTOs/InventoryCatalogDTO.cs | 2 +- .../Inventory/DTOs/InventoryItemDTO.cs | 3 +- .../Configurations/IngredientConfiguration.cs | 2 +- .../Inventory/Entities/Ingredient.cs | 2 +- .../Inventory/Entities/InventoryBatch.cs | 2 +- .../Entities/InventoryTransaction.cs | 2 +- .../Inventory/Entities/LocationUnit.cs | 19 + CulinaryCommandApp/Inventory/Entities/Unit.cs | 6 +- .../Pages/Inventory/InventoryCatalog.razor | 251 +++- .../Pages/Inventory/InventoryManagement.razor | 8 +- .../Inventory/Services/IngredientService.cs | 17 +- .../Services/Interfaces/IIngredientService.cs | 7 +- .../Interfaces/IInventoryManagementService.cs | 4 +- .../IInventoryTransactionService.cs | 13 +- .../Services/Interfaces/IUnitService.cs | 9 +- .../Services/InventoryManagementService.cs | 18 +- .../Services/InventoryTransactionService.cs | 75 +- .../Inventory/Services/UnitService.cs | 46 +- ...2_RecipeAndLocationUnitSupport.Designer.cs | 1121 ++++++++++++++++ ...0227192322_RecipeAndLocationUnitSupport.cs | 346 +++++ ...0305191150_AddRecipeRowVersion.Designer.cs | 1126 +++++++++++++++++ .../20260305191150_AddRecipeRowVersion.cs | 31 + .../20260306015138_SeedUnits.Designer.cs | 1126 +++++++++++++++++ .../Migrations/20260306015138_SeedUnits.cs | 48 + .../Migrations/AppDbContextModelSnapshot.cs | 806 ++++++------ CulinaryCommandApp/Program.cs | 8 +- .../Entities/PurchaseOrderLine.cs | 2 +- .../PurchaseOrder/Pages/Create.razor | 2 +- .../Components/ProduceRecipeDialog.razor | 247 ++++ CulinaryCommandApp/Recipe/Entities/Recipe.cs | 45 + .../Recipe/Entities/RecipeIngredient.cs | 30 + .../Recipe/Entities/RecipeStep.cs | 32 + .../Recipe/Entities/RecipeSubRecipe.cs | 12 + .../Recipe/Pages/IngredientLineRow.razor | 117 ++ .../Recipe/Pages/RecipeCreate.razor | 83 ++ .../Recipe/Pages/RecipeEdit.razor | 103 ++ .../Recipe/Pages/RecipeForm.razor | 820 ++++++++++++ .../Recipe/Pages/RecipeList.razor | 486 +++++++ .../Recipe/Pages/RecipeList.razor.css | 391 ++++++ .../Recipe/Pages/RecipeView.razor | 257 ++++ .../Recipe/Pages/RecipeView.razor.css | 321 +++++ .../Recipe/Pages/_Imports.razor | 10 + .../Services/Interfaces/IRecipeService.cs | 51 + .../Recipe/Services/RecipeService.cs | 151 +++ CulinaryCommandApp/Services/RecipeService.cs | 68 - .../DTOs/CreateIngredientDTOTests.cs | 3 +- .../DTOs/InventoryCatalogDTOTests.cs | 2 +- .../Inventory/DTOs/InventoryItemDTOTests.cs | 2 +- .../Inventory/Entities/IngredientTest.cs | 2 +- .../Entities/InventoryTransactionTest.cs | 2 +- .../Inventory/Entities/UnitTest.cs | 2 +- 71 files changed, 8130 insertions(+), 1347 deletions(-) create mode 100644 CulinaryCommandApp/Components/Pages/EmployeeView.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/Create.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/Edit.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/Index.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/RecipeForm.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/RecipeList.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor delete mode 100644 CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor.css delete mode 100644 CulinaryCommandApp/Data/Entities/Ingredient.cs delete mode 100644 CulinaryCommandApp/Data/Entities/MeasurementUnit.cs delete mode 100644 CulinaryCommandApp/Data/Entities/Recipe.cs delete mode 100644 CulinaryCommandApp/Data/Entities/RecipeIngredient.cs delete mode 100644 CulinaryCommandApp/Data/Entities/RecipeStep.cs delete mode 100644 CulinaryCommandApp/Data/Models/MeasurementUnitViewModel.cs create mode 100644 CulinaryCommandApp/Inventory/Entities/LocationUnit.cs create mode 100644 CulinaryCommandApp/Migrations/20260227192322_RecipeAndLocationUnitSupport.Designer.cs create mode 100644 CulinaryCommandApp/Migrations/20260227192322_RecipeAndLocationUnitSupport.cs create mode 100644 CulinaryCommandApp/Migrations/20260305191150_AddRecipeRowVersion.Designer.cs create mode 100644 CulinaryCommandApp/Migrations/20260305191150_AddRecipeRowVersion.cs create mode 100644 CulinaryCommandApp/Migrations/20260306015138_SeedUnits.Designer.cs create mode 100644 CulinaryCommandApp/Migrations/20260306015138_SeedUnits.cs create mode 100644 CulinaryCommandApp/Recipe/Components/ProduceRecipeDialog.razor create mode 100644 CulinaryCommandApp/Recipe/Entities/Recipe.cs create mode 100644 CulinaryCommandApp/Recipe/Entities/RecipeIngredient.cs create mode 100644 CulinaryCommandApp/Recipe/Entities/RecipeStep.cs create mode 100644 CulinaryCommandApp/Recipe/Entities/RecipeSubRecipe.cs create mode 100644 CulinaryCommandApp/Recipe/Pages/IngredientLineRow.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeCreate.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeEdit.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeForm.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeList.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeList.razor.css create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeView.razor create mode 100644 CulinaryCommandApp/Recipe/Pages/RecipeView.razor.css create mode 100644 CulinaryCommandApp/Recipe/Pages/_Imports.razor create mode 100644 CulinaryCommandApp/Recipe/Services/Interfaces/IRecipeService.cs create mode 100644 CulinaryCommandApp/Recipe/Services/RecipeService.cs delete mode 100644 CulinaryCommandApp/Services/RecipeService.cs diff --git a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor index 3b73c89..9480631 100644 --- a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor +++ b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor @@ -4,13 +4,16 @@ @using CulinaryCommand.Services @using CulinaryCommand.Data.Enums; @using CulinaryCommand.Services.UserContextSpace +@using CulinaryCommandApp.Recipe.Services; +@using CulinaryCommandApp.Recipe.Services.Interfaces; +@using CulinaryCommandApp.Recipe.Entities; @inject IUserContextService UserCtx @inject NavigationManager Nav @inject ILocationService LocationService @inject IUserService UserService @inject LocationState LocationState @inject ITaskAssignmentService TaskService -@inject RecipeService RecipeService +@inject IRecipeService RecipeService @implements IDisposable @rendermode InteractiveServer @@ -346,11 +349,8 @@ else try { - var all = await RecipeService.GetAllAsync(); - recipes = all - .Where(r => r.LocationId == selectedLocationId.Value) - .OrderBy(r => r.Title) - .ToList(); + recipes = await RecipeService.GetAllByLocationIdAsync(selectedLocationId.Value); + recipes = recipes.OrderBy(r => r.Title).ToList(); } catch { diff --git a/CulinaryCommandApp/Components/Pages/EmployeeView.razor b/CulinaryCommandApp/Components/Pages/EmployeeView.razor new file mode 100644 index 0000000..82f700d --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/EmployeeView.razor @@ -0,0 +1,64 @@ +@rendermode InteractiveServer + +@using CulinaryCommand.Components.Custom +@using CulinaryCommand.Services.UserContextSpace +@using CulinaryCommand.Services + +@inject IUserContextService UserCtx +@inject LocationState LocationState + +
+

Employee Dashboard

+ + @if (!_ready) + { +
Loading...
+ } + else if (LocationState.CurrentLocation is null) + { +

No location assigned. Please contact your manager.

+ } + else + { +

Welcome to @LocationState.CurrentLocation.Name.

+ +
+
+
+ + +
+
+ + +
+
+
+ } +
+ +@code { + + private bool _ready; + + protected override async Task OnInitializedAsync() + { + _ready = true; + } +} diff --git a/CulinaryCommandApp/Components/Pages/Recipes/Create.razor b/CulinaryCommandApp/Components/Pages/Recipes/Create.razor deleted file mode 100644 index 8df7d49..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/Create.razor +++ /dev/null @@ -1,31 +0,0 @@ -@page "/recipes/create" -@using CulinaryCommand.Data.Entities -@inject NavigationManager Nav -@inject RecipeService Recipes -@inject LocationState LocationState -@rendermode InteractiveServer - -

Create Recipe

- - - -@code { - private Recipe model = new(); - - private async Task Save() - { - // Make sure we have a current location - if (LocationState.CurrentLocation is null) - { - // You can swap this for a nicer UI message later - Console.WriteLine("No current location selected – cannot save recipe."); - return; - } - - // πŸ”₯ attach recipe to the active location - model.LocationId = LocationState.CurrentLocation.Id; - - await Recipes.CreateAsync(model); - Nav.NavigateTo("/recipes"); - } -} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Recipes/Edit.razor b/CulinaryCommandApp/Components/Pages/Recipes/Edit.razor deleted file mode 100644 index 95b1205..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/Edit.razor +++ /dev/null @@ -1,33 +0,0 @@ -@page "/recipes/edit/{id:int}" -@using CulinaryCommand.Data.Entities -@inject NavigationManager Nav -@inject RecipeService Recipes -@rendermode InteractiveServer - -

Edit Recipe

- -@if (model == null) -{ -

Loading...

-} -else -{ - -} - -@code { - [Parameter] public int id { get; set; } - - private Recipe? model; - - protected override async Task OnInitializedAsync() - { - model = await Recipes.GetByIdAsync(id); - } - - private async Task Save() - { - await Recipes.UpdateAsync(model); - Nav.NavigateTo("/recipes"); - } -} diff --git a/CulinaryCommandApp/Components/Pages/Recipes/Index.razor b/CulinaryCommandApp/Components/Pages/Recipes/Index.razor deleted file mode 100644 index 7da5afe..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/Index.razor +++ /dev/null @@ -1,5 +0,0 @@ -@page "/recipes" -@using CulinaryCommand.Data.Entities -@rendermode InteractiveServer - - diff --git a/CulinaryCommandApp/Components/Pages/Recipes/RecipeForm.razor b/CulinaryCommandApp/Components/Pages/Recipes/RecipeForm.razor deleted file mode 100644 index bc08ae7..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/RecipeForm.razor +++ /dev/null @@ -1,234 +0,0 @@ -@using CulinaryCommand.Data.Entities -@using CulinaryCommand.Inventory.Services -@rendermode InteractiveServer -@inject NavigationManager Nav -@inject IngredientService Ingredients -@inject UnitService Units -@inject EnumService enumService - -
-
-

@FormTitle

-
- -
- - -
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - - Enter a value from 0–100% -
- -
- - -
-
- -
- - -
Ingredients
- - @foreach (var item in Model.RecipeIngredients.OrderBy(i => i.SortOrder)) - { -
- - -
- - -
- - -
- - -
- - -
- - -
- -
-
- - -
-
- - -
-
- - -
- } - - - -
- - -
Steps
- - @foreach (var step in Model.Steps.OrderBy(s => s.StepNumber)) - { -
- - - -
- } - - - -
- - - - - -
-
- -@code { - [Parameter] public Recipe Model { get; set; } = new(); - [Parameter] public EventCallback OnValidSubmit { get; set; } - [Parameter] public string FormTitle { get; set; } = "Recipe"; - - public List IngredientCategories { get; set; } = new(); - private List recipeCategories = new (); - private List recipeTypes = new (); - private List yieldUnits = new(); - - protected override async Task OnInitializedAsync() - { - IngredientCategories = enumService.GetCategories(); - recipeCategories = enumService.GetCategories(); - recipeTypes = enumService.GetRecipeTypes(); - yieldUnits = enumService.GetUnits(); - } - - void AddIngredient() - { - Model.RecipeIngredients.Add(new RecipeIngredient - { - SortOrder = Model.RecipeIngredients.Count + 1, - AvailableIngredients = new(), - AvailableUnits = new() - }); - } - - void RemoveIngredient(RecipeIngredient item) - { - Model.RecipeIngredients.Remove(item); - } - - void AddStep() - { - Model.Steps.Add(new RecipeStep - { - StepNumber = Model.Steps.Count + 1 - }); - } - - void RemoveStep(RecipeStep step) - { - Model.Steps.Remove(step); - } - - async Task LoadIngredientsForCategory(RecipeIngredient item, string? category) - { - if (string.IsNullOrWhiteSpace(category)) - return; - - item.AvailableIngredients = await Ingredients.GetByCategoryAsync(category); - } - - async Task LoadUnitsForIngredient(RecipeIngredient item) - { - if (item.IngredientId <= 0) - return; - - item.AvailableUnits = await Units.GetUnitsForIngredient(item.IngredientId); - } - - void Cancel() => Nav.NavigateTo("/recipes"); - - async Task Save() => await OnValidSubmit.InvokeAsync(); -} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Recipes/RecipeList.razor b/CulinaryCommandApp/Components/Pages/Recipes/RecipeList.razor deleted file mode 100644 index 4705381..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/RecipeList.razor +++ /dev/null @@ -1,167 +0,0 @@ -@* @page "/recipes" *@ -@rendermode InteractiveServer -@implements IDisposable - -@using CulinaryCommand.Data.Entities -@using CulinaryCommand.Services -@using CulinaryCommand.Services.UserContextSpace - -@inject NavigationManager Nav -@inject IJSRuntime Js -@inject RecipeService Recipes -@inject LocationState LocationState -@inject IUserContextService UserCtx - -

Recipes

- -@if (priv) -{ - -} - -@if (recipes == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - - @foreach (var r in recipes) - { - - - - - - - - } - -
TitleCategoryTypeYield
@r.Title@r.Category@r.RecipeType@FormatYield(r) - - @if (priv) - { - - - } -
-} - -@code { - private UserContext? _ctx; - - private List? recipes; - private bool priv = false; - - protected override async Task OnInitializedAsync() - { - _ctx = await UserCtx.GetAsync(); - - // If not authenticated, bounce to Cognito login - if (_ctx.IsAuthenticated != true) - { - Nav.NavigateTo("/login", true); - return; - } - - // Invite-only: authenticated but not in DB - if (_ctx.User is null) - { - Nav.NavigateTo("/no-access", true); - return; - } - - // Ensure LocationState is hydrated if user deep-links here - if (LocationState.ManagedLocations.Count == 0 && _ctx.AccessibleLocations.Any()) - await LocationState.SetLocationsAsync(_ctx.AccessibleLocations); - - UpdatePriv(); - - LocationState.OnChange += RefreshRecipes; - - await LoadRecipesAsync(); - } - - private void UpdatePriv() - { - var role = _ctx?.User?.Role; - priv = - string.Equals(role, "Manager", StringComparison.OrdinalIgnoreCase) || - string.Equals(role, "Admin", StringComparison.OrdinalIgnoreCase); - } - - private async Task LoadRecipesAsync() - { - var loc = LocationState.CurrentLocation; - - if (loc == null) - { - recipes = new List(); - return; - } - - recipes = await Recipes.GetAllByLocationIdAsync(loc.Id); - } - - private void RefreshRecipes() - { - _ = InvokeAsync(async () => - { - await LoadRecipesAsync(); - StateHasChanged(); - }); - } - - void AddNew() => Nav.NavigateTo("/recipes/create"); - void Edit(int id) => Nav.NavigateTo($"/recipes/edit/{id}"); - void View(int id) => Nav.NavigateTo($"/recipes/view/{id}"); - - async Task Delete(int id) - { - if (await Js.InvokeAsync("confirm", $"Delete recipe #{id}?")) - { - await Recipes.DeleteAsync(id); - await LoadRecipesAsync(); - await InvokeAsync(StateHasChanged); - } - } - - private string FormatYield(Recipe r) - { - if (!r.YieldAmount.HasValue) - return string.IsNullOrWhiteSpace(r.YieldUnit) ? string.Empty : r.YieldUnit!; - - var amount = r.YieldAmount.Value; - - var amountText = decimal.Truncate(amount) == amount - ? amount.ToString("0") - : amount.ToString("0.##"); - - return string.IsNullOrWhiteSpace(r.YieldUnit) - ? amountText - : $"{amountText} {r.YieldUnit}"; - } - - public void Dispose() - { - LocationState.OnChange -= RefreshRecipes; - } -} diff --git a/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor b/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor deleted file mode 100644 index ba7b904..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor +++ /dev/null @@ -1,133 +0,0 @@ -@page "/recipes/view/{id:int}" -@using CulinaryCommand.Data.Entities -@inject RecipeService Recipes -@inject NavigationManager Nav - -
- -
-

@recipe?.Title

- -
- - @if (recipe == null) - { -

Loading...

- } - else - { -
-
- - -
-
-
-
Category
-
@recipe.Category
-
-
- -
-
-
Type
-
@recipe.RecipeType
-
-
- -
-
-
Yield
-
@FormatYield(recipe)
-
-
-
- -
- - -

- Ingredients -

- - @if (recipe.RecipeIngredients.Count == 0) - { -

No ingredients added.

- } - else - { -
    - @foreach (var ing in recipe.RecipeIngredients.OrderBy(i => i.SortOrder)) - { -
  • - @FormatQuantity(ing.Quantity) - @(" " + ing.Unit?.Name) - @(" β€” ") - @ing.Ingredient?.Name - - @if (!string.IsNullOrWhiteSpace(ing.PrepNote)) - { -
    @ing.PrepNote
    - } -
  • - } -
- } - - -

- Steps -

- - @if (recipe.Steps.Count == 0) - { -

No steps added.

- } - else - { -
    - @foreach (var step in recipe.Steps.OrderBy(s => s.StepNumber)) - { -
  1. -
    @step.Instructions
    -
  2. - } -
- } - -
-
- } -
- -@code { - [Parameter] public int id { get; set; } - private Recipe? recipe; - - protected override async Task OnInitializedAsync() - { - recipe = await Recipes.GetByIdAsync(id); - } - - void GoBack() => Nav.NavigateTo("/recipes"); - - string FormatQuantity(decimal qty) - { - // remove trailing zeros - return qty.ToString("0.##"); - } - - string FormatYield(Recipe r) - { - if (!r.YieldAmount.HasValue) - return r.YieldUnit ?? ""; - - string amt = r.YieldAmount.Value.ToString("0.##"); - - return string.IsNullOrWhiteSpace(r.YieldUnit) - ? amt - : $"{amt} {r.YieldUnit}"; - } -} diff --git a/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor.css b/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor.css deleted file mode 100644 index 4669abd..0000000 --- a/CulinaryCommandApp/Components/Pages/Recipes/RecipeView.razor.css +++ /dev/null @@ -1,10 +0,0 @@ -.detail-box { - background: #f8f9fa; - border: 1px solid #e3e3e3; -} - -.list-group-numbered > .list-group-item { - padding: 1rem; - border-radius: 6px; - margin-bottom: 6px; -} diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor b/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor index dd09708..7cea837 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor @@ -5,6 +5,9 @@ @using CulinaryCommand.Vendor.Services @using CulinaryCommand.Services @using CulinaryCommand.Services.UserContextSpace +@using CulinaryCommandApp.Inventory.Entities +@using CulinaryCommandApp.Inventory.Services +@using CulinaryCommandApp.Inventory.Services.Interfaces @using Microsoft.AspNetCore.Authorization @using System.Net.Http.Json @using System.Text.Json @@ -12,6 +15,7 @@ @inject IVendorService VendorService @inject ILocationService LocationService +@inject IUnitService UnitService @inject IUserContextService UserCtx @inject NavigationManager Nav @inject IConfiguration Config @@ -132,6 +136,61 @@ } + + +
+
+
Measurement Units
+ +
+ +
+ @if (!_locationUnits.Any()) + { +
+ + No units assigned to this location yet. Add one to get started. +
+ } + else + { + + + + + + + + + + @foreach (var unit in _locationUnits) + { + + + + + + } + +
NameAbbreviation
@unit.Name@unit.Abbreviation + +
+ } +
+ + @if (_locationUnits.Any()) + { + + } +
} @@ -261,15 +320,66 @@ } + +@if (_showAddUnitModal) +{ + +} + @code { [Parameter] public int LocationId { get; set; } private Location? _location; private List _locationVendors = new(); + private List _locationUnits = new(); + private List _availableUnitsToAdd = new(); private bool _hydrated; private bool _showAddVendorModal; private bool _creatingVendor; + private bool _showAddUnitModal; + private bool _addingUnit; + private int _selectedUnitId; private Vendor _newVendor = new(); private int? _companyId; @@ -313,6 +423,7 @@ if (_location is not null && _companyId.HasValue) { _locationVendors = await VendorService.GetVendorsByLocationAsync(LocationId); + _locationUnits = await UnitService.GetByLocationAsync(LocationId); } _hydrated = true; @@ -445,4 +556,45 @@ _showAddVendorModal = false; StateHasChanged(); } + + // ── Units ───────────────────────────────────────────────────────────────── + + private async Task OpenAddUnitModal() + { + var all = await UnitService.GetAllAsync(); + var assignedIds = _locationUnits.Select(u => u.Id).ToHashSet(); + _availableUnitsToAdd = all.Where(u => !assignedIds.Contains(u.Id)).ToList(); + _selectedUnitId = 0; + _showAddUnitModal = true; + } + + private void CloseAddUnitModal() + { + _showAddUnitModal = false; + _selectedUnitId = 0; + } + + private async Task AddUnit() + { + if (_selectedUnitId == 0) return; + + _addingUnit = true; + + var currentIds = _locationUnits.Select(u => u.Id).ToList(); + currentIds.Add(_selectedUnitId); + await UnitService.SetLocationUnitsAsync(LocationId, currentIds); + _locationUnits = await UnitService.GetByLocationAsync(LocationId); + + _addingUnit = false; + _showAddUnitModal = false; + StateHasChanged(); + } + + private async Task RemoveUnit(int unitId) + { + var remaining = _locationUnits.Select(u => u.Id).Where(id => id != unitId); + await UnitService.SetLocationUnitsAsync(LocationId, remaining); + _locationUnits = await UnitService.GetByLocationAsync(LocationId); + StateHasChanged(); + } } diff --git a/CulinaryCommandApp/Data/AppDbContext.cs b/CulinaryCommandApp/Data/AppDbContext.cs index c37eb03..3ddb33e 100644 --- a/CulinaryCommandApp/Data/AppDbContext.cs +++ b/CulinaryCommandApp/Data/AppDbContext.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; using CulinaryCommand.Data.Entities; -using CulinaryCommand.Inventory.Entities; +using CulinaryCommandApp.Inventory.Entities; +using CulinaryCommandApp.Recipe.Entities; using PO = CulinaryCommand.PurchaseOrder.Entities; using V = CulinaryCommand.Vendor.Entities; + namespace CulinaryCommand.Data { public class AppDbContext : DbContext @@ -17,20 +19,20 @@ public AppDbContext(DbContextOptions options) public DbSet Users => Set(); public DbSet Tasks => Set(); public DbSet Companies => Set(); - public DbSet Ingredients => Set(); - public DbSet MeasurementUnits => Set(); - public DbSet Recipes => Set(); - public DbSet RecipeIngredients => Set(); - public DbSet RecipeSteps => Set(); + public DbSet Ingredients => Set(); + public DbSet Recipes => Set(); + public DbSet RecipeIngredients => Set(); + public DbSet RecipeSteps => Set(); + public DbSet RecipeSubRecipes => Set(); public DbSet UserLocations => Set(); public DbSet ManagerLocations => Set(); public DbSet InventoryTransactions => Set(); - public DbSet Units => Set(); + public DbSet Units => Set(); public DbSet PurchaseOrders => Set(); public DbSet PurchaseOrderLines => Set(); public DbSet Vendors => Set(); public DbSet LocationVendors => Set(); - + public DbSet LocationUnits => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -80,14 +82,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasForeignKey(ml => ml.LocationId); // Ingredient belongs to a Location - modelBuilder.Entity() + modelBuilder.Entity() .HasOne(i => i.Location) .WithMany() .HasForeignKey(i => i.LocationId) .OnDelete(DeleteBehavior.Cascade); // Ingredient optionally belongs to a Vendor - modelBuilder.Entity() + modelBuilder.Entity() .HasOne(i => i.Vendor) .WithMany() .HasForeignKey(i => i.VendorId) @@ -114,7 +116,101 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasOne(lv => lv.Vendor) .WithMany(v => v.LocationVendors) .HasForeignKey(lv => lv.VendorId); - + + // Recipe belongs to a Location + modelBuilder.Entity() + .HasOne(r => r.Location) + .WithMany(l => l.Recipes) + .HasForeignKey(r => r.LocationId) + .OnDelete(DeleteBehavior.Cascade); + + // RowVersion is a MySQL timestamp(6) with DEFAULT/ON UPDATE CURRENT_TIMESTAMP(6). + // Mark it as database-generated so EF never sends DateTime.MinValue on INSERT/UPDATE. + modelBuilder.Entity() + .Property(r => r.RowVersion) + .ValueGeneratedOnAddOrUpdate() + .IsConcurrencyToken(); + + // Tasks optionally references a Recipe (prep task) + modelBuilder.Entity() + .HasOne(t => t.Recipe) + .WithMany() + .HasForeignKey(t => t.RecipeId) + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(false); + + // RecipeIngredient to parent Recipe + modelBuilder.Entity() + .HasOne(ri => ri.Recipe) + .WithMany(r => r.RecipeIngredients) + .HasForeignKey(ri => ri.RecipeId) + .OnDelete(DeleteBehavior.Cascade); + + // RecipeIngredient - optional Ingredient (raw ingredient line) + modelBuilder.Entity() + .HasOne(ri => ri.Ingredient) + .WithMany() + .HasForeignKey(ri => ri.IngredientId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + // RecipeIngredient - optional sub-Recipe + modelBuilder.Entity() + .HasOne(ri => ri.SubRecipe) + .WithMany() + .HasForeignKey(ri => ri.SubRecipeId) + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(false); + + // RecipeIngredient - Unit + modelBuilder.Entity() + .HasOne(ri => ri.Unit) + .WithMany() + .HasForeignKey(ri => ri.UnitId) + .OnDelete(DeleteBehavior.Restrict); + + // RecipeStep - Recipe + modelBuilder.Entity() + .HasOne(rs => rs.Recipe) + .WithMany(r => r.Steps) + .HasForeignKey(rs => rs.RecipeId) + .OnDelete(DeleteBehavior.Cascade); + + // RecipeSubRecipe: composite PK + modelBuilder.Entity() + .HasKey(rs => new { rs.ParentRecipeId, rs.ChildRecipeId }); + + // RecipeSubRecipe - parent Recipe (cascade: deleting a parent removes its sub-recipe links) + modelBuilder.Entity() + .HasOne(rs => rs.ParentRecipe) + .WithMany(r => r.SubRecipeUsages) + .HasForeignKey(rs => rs.ParentRecipeId) + .OnDelete(DeleteBehavior.Cascade); + + // RecipeSubRecipe - child Recipe (restrict: cannot delete a sub-recipe still in use) + modelBuilder.Entity() + .HasOne(rs => rs.ChildRecipe) + .WithMany(r => r.UsedInRecipes) + .HasForeignKey(rs => rs.ChildRecipeId) + .OnDelete(DeleteBehavior.Restrict); + + ConfigureLocationUnit(modelBuilder); + } + + private void ConfigureLocationUnit(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasKey(lu => new { lu.LocationId, lu.UnitId }); + + modelBuilder.Entity() + .HasOne(lu => lu.Location) + .WithMany(l => l.LocationUnits) + .HasForeignKey(lu => lu.LocationId); + + modelBuilder.Entity() + .HasOne(lu => lu.Unit) + .WithMany(u => u.LocationUnits) + .HasForeignKey(lu => lu.UnitId); } } } \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/Ingredient.cs b/CulinaryCommandApp/Data/Entities/Ingredient.cs deleted file mode 100644 index 944da5b..0000000 --- a/CulinaryCommandApp/Data/Entities/Ingredient.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace CulinaryCommand.Data.Entities -{ - public class Ingredient - { - [Key] - public int IngredientId { get; set; } - - [Required, MaxLength(128)] - public string Name { get; set; } - - [Required, MaxLength(128)] - public string Category { get; set; } - - [Required, MaxLength(128)] - public string DefaultUnit { get; set; } - - // Navigation - public ICollection RecipeIngredients { get; set; } = new List(); - } -} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/Location.cs b/CulinaryCommandApp/Data/Entities/Location.cs index 8d84094..2025d93 100644 --- a/CulinaryCommandApp/Data/Entities/Location.cs +++ b/CulinaryCommandApp/Data/Entities/Location.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; +using CulinaryCommandApp.Inventory.Entities; +using Rec = CulinaryCommandApp.Recipe.Entities; namespace CulinaryCommand.Data.Entities { @@ -31,7 +33,7 @@ public class Location public int CompanyId { get; set; } public Company Company { get; set; } - public ICollection Recipes { get; set; } = new List(); + public ICollection Recipes { get; set; } = new List(); // join table combining employees and locations [JsonIgnore] @@ -53,6 +55,8 @@ public class Location [JsonIgnore] public ICollection LocationVendors { get; set; } = new List(); - } + [JsonIgnore] + public ICollection LocationUnits { get; set; } = new List(); + } } diff --git a/CulinaryCommandApp/Data/Entities/MeasurementUnit.cs b/CulinaryCommandApp/Data/Entities/MeasurementUnit.cs deleted file mode 100644 index 5413550..0000000 --- a/CulinaryCommandApp/Data/Entities/MeasurementUnit.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace CulinaryCommand.Data.Entities -{ - public class MeasurementUnit - { - [Key] - public int UnitId { get; set; } - - [Required, MaxLength(128)] - public string Name { get; set; } // "Cup" - - [Required, MaxLength(32)] - public string Abbreviation { get; set; } // "c" - - // Navigation - public ICollection RecipeIngredients { get; set; } = new List(); - } - -} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/Recipe.cs b/CulinaryCommandApp/Data/Entities/Recipe.cs deleted file mode 100644 index 60b15a8..0000000 --- a/CulinaryCommandApp/Data/Entities/Recipe.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace CulinaryCommand.Data.Entities -{ - public class Recipe - { - [Key] - public int RecipeId { get; set; } - public int LocationId { get; set; } - - [Required, MaxLength(128)] - public string? Title { get; set; } - - [Required, MaxLength(128)] - public string Category { get; set; } - - [Required, MaxLength(128)] - public string RecipeType { get; set; } - - public decimal? YieldAmount { get; set; } - - [MaxLength(128)] - public string YieldUnit { get; set; } - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - // Navigation - public Location? Location { get; set; } - public ICollection RecipeIngredients { get; set; } = new List(); - public ICollection Steps { get; set; } = new List(); - } - -} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/RecipeIngredient.cs b/CulinaryCommandApp/Data/Entities/RecipeIngredient.cs deleted file mode 100644 index d166a1b..0000000 --- a/CulinaryCommandApp/Data/Entities/RecipeIngredient.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Threading.Tasks; -using CulinaryCommand.Inventory.Entities; - -namespace CulinaryCommand.Data.Entities -{ - public class RecipeIngredient - { - [Key] - public int RecipeIngredientId { get; set; } - public int RecipeId { get; set; } - public int IngredientId { get; set; } - public int UnitId { get; set; } - - public decimal Quantity { get; set; } - - [MaxLength(256)] - public string? PrepNote { get; set; } - public int SortOrder { get; set; } - - // Navigation - public Recipe? Recipe { get; set; } - public CulinaryCommand.Inventory.Entities.Ingredient? Ingredient { get; set; } - public Unit? Unit { get; set; } - - [NotMapped] - public List AvailableIngredients { get; set; } = new(); - - [NotMapped] - public List AvailableUnits { get; set; } = new(); - } - -} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/RecipeStep.cs b/CulinaryCommandApp/Data/Entities/RecipeStep.cs deleted file mode 100644 index 8a25b2e..0000000 --- a/CulinaryCommandApp/Data/Entities/RecipeStep.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Threading.Tasks; - -namespace CulinaryCommand.Data.Entities -{ - public class RecipeStep - { - [Key] - public int StepId { get; set; } - public int RecipeId { get; set; } - - public int StepNumber { get; set; } // 1, 2, 3... - - [MaxLength(256)] - public string? Instructions { get; set; } - - // Navigation - public Recipe? Recipe { get; set; } - } - -} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/Tasks.cs b/CulinaryCommandApp/Data/Entities/Tasks.cs index 4e5d78a..af8586d 100644 --- a/CulinaryCommandApp/Data/Entities/Tasks.cs +++ b/CulinaryCommandApp/Data/Entities/Tasks.cs @@ -1,7 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using CulinaryCommand.Data.Enums; -using InvIngredient = CulinaryCommand.Inventory.Entities.Ingredient; +using Rec = CulinaryCommandApp.Recipe.Entities; +using InvIngredient = CulinaryCommandApp.Inventory.Entities.Ingredient; namespace CulinaryCommand.Data.Entities { @@ -52,7 +53,7 @@ public class Tasks // Link to recipe if this is a prep task public int? RecipeId { get; set; } - public Recipe? Recipe { get; set; } + public Rec.Recipe? Recipe { get; set; } // Link to ingredient (Inventory.Entities.Ingredient) public int? IngredientId { get; set; } diff --git a/CulinaryCommandApp/Data/Models/MeasurementUnitViewModel.cs b/CulinaryCommandApp/Data/Models/MeasurementUnitViewModel.cs deleted file mode 100644 index d311c2f..0000000 --- a/CulinaryCommandApp/Data/Models/MeasurementUnitViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace CulinaryCommand.Models -{ - public class MeasurementUnitViewModel - { - public int UnitId { get; set; } - public string UnitName { get; set; } = ""; - public string Abbreviation { get; set; } = ""; - } - -} \ No newline at end of file diff --git a/CulinaryCommandApp/Inventory/DTOs/CreateIngredientDTO.cs b/CulinaryCommandApp/Inventory/DTOs/CreateIngredientDTO.cs index 84abe90..4443575 100644 --- a/CulinaryCommandApp/Inventory/DTOs/CreateIngredientDTO.cs +++ b/CulinaryCommandApp/Inventory/DTOs/CreateIngredientDTO.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace CulinaryCommand.Inventory.DTOs +namespace CulinaryCommandApp.Inventory.DTOs { public class CreateIngredientDTO { diff --git a/CulinaryCommandApp/Inventory/DTOs/InventoryCatalogDTO.cs b/CulinaryCommandApp/Inventory/DTOs/InventoryCatalogDTO.cs index 0aaf14d..ac8398f 100644 --- a/CulinaryCommandApp/Inventory/DTOs/InventoryCatalogDTO.cs +++ b/CulinaryCommandApp/Inventory/DTOs/InventoryCatalogDTO.cs @@ -1,6 +1,6 @@ using System; -namespace CulinaryCommand.Inventory.DTOs +namespace CulinaryCommandApp.Inventory.DTOs { public class InventoryCatalogDTO { diff --git a/CulinaryCommandApp/Inventory/DTOs/InventoryItemDTO.cs b/CulinaryCommandApp/Inventory/DTOs/InventoryItemDTO.cs index 0304346..a15276f 100644 --- a/CulinaryCommandApp/Inventory/DTOs/InventoryItemDTO.cs +++ b/CulinaryCommandApp/Inventory/DTOs/InventoryItemDTO.cs @@ -1,6 +1,6 @@ using System; -namespace CulinaryCommand.Inventory.DTOs +namespace CulinaryCommandApp.Inventory.DTOs { public class InventoryItemDTO { @@ -10,6 +10,7 @@ public class InventoryItemDTO public string Category { get; set; } = string.Empty; public decimal CurrentQuantity { get; set; } public string Unit { get; set; } = "count"; + public int UnitId { get; set; } public decimal Price { get; set; } public decimal ReorderLevel { get; set; } public bool IsLowStock { get; set; } diff --git a/CulinaryCommandApp/Inventory/Data/Configurations/IngredientConfiguration.cs b/CulinaryCommandApp/Inventory/Data/Configurations/IngredientConfiguration.cs index c4f06b7..d4cb11a 100644 --- a/CulinaryCommandApp/Inventory/Data/Configurations/IngredientConfiguration.cs +++ b/CulinaryCommandApp/Inventory/Data/Configurations/IngredientConfiguration.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using CulinaryCommand.Inventory.Entities; +using CulinaryCommandApp.Inventory.Entities; namespace CulinaryCommand.Inventory.Data.Configurations { diff --git a/CulinaryCommandApp/Inventory/Entities/Ingredient.cs b/CulinaryCommandApp/Inventory/Entities/Ingredient.cs index 11df568..a58d097 100644 --- a/CulinaryCommandApp/Inventory/Entities/Ingredient.cs +++ b/CulinaryCommandApp/Inventory/Entities/Ingredient.cs @@ -1,7 +1,7 @@ using System; using CulinaryCommand.Data.Entities; -namespace CulinaryCommand.Inventory.Entities +namespace CulinaryCommandApp.Inventory.Entities { public class Ingredient { diff --git a/CulinaryCommandApp/Inventory/Entities/InventoryBatch.cs b/CulinaryCommandApp/Inventory/Entities/InventoryBatch.cs index 57c3a09..ff34b02 100644 --- a/CulinaryCommandApp/Inventory/Entities/InventoryBatch.cs +++ b/CulinaryCommandApp/Inventory/Entities/InventoryBatch.cs @@ -2,7 +2,7 @@ using CulinaryCommand.Data.Entities; -namespace CulinaryCommand.Inventory.Entities +namespace CulinaryCommandApp.Inventory.Entities { public class InventoryBatch { diff --git a/CulinaryCommandApp/Inventory/Entities/InventoryTransaction.cs b/CulinaryCommandApp/Inventory/Entities/InventoryTransaction.cs index 3f327d3..4ed294d 100644 --- a/CulinaryCommandApp/Inventory/Entities/InventoryTransaction.cs +++ b/CulinaryCommandApp/Inventory/Entities/InventoryTransaction.cs @@ -1,6 +1,6 @@ using System; -namespace CulinaryCommand.Inventory.Entities +namespace CulinaryCommandApp.Inventory.Entities { public class InventoryTransaction diff --git a/CulinaryCommandApp/Inventory/Entities/LocationUnit.cs b/CulinaryCommandApp/Inventory/Entities/LocationUnit.cs new file mode 100644 index 0000000..7fe3928 --- /dev/null +++ b/CulinaryCommandApp/Inventory/Entities/LocationUnit.cs @@ -0,0 +1,19 @@ +using System.Text.Json.Serialization; +using CulinaryCommand.Data.Entities; +using CulinaryCommandApp.Inventory.Entities; + +namespace CulinaryCommandApp.Inventory.Entities +{ + public class LocationUnit + { + [JsonIgnore] + public Location Location { get; set; } = default!; + public int LocationId { get; set; } + + [JsonIgnore] + public Unit Unit { get; set; } = default!; + public int UnitId { get; set; } + + public DateTime AssignedAt { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Inventory/Entities/Unit.cs b/CulinaryCommandApp/Inventory/Entities/Unit.cs index 171f42a..676f666 100644 --- a/CulinaryCommandApp/Inventory/Entities/Unit.cs +++ b/CulinaryCommandApp/Inventory/Entities/Unit.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; -namespace CulinaryCommand.Inventory.Entities +namespace CulinaryCommandApp.Inventory.Entities { public class Unit { @@ -22,5 +23,8 @@ public class Unit // list of inventory transactions that use this unit public ICollection InventoryTransaction { get; set; } = new List(); + [JsonIgnore] + public ICollection LocationUnits { get; set; } = new List(); + } } \ No newline at end of file diff --git a/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor b/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor index c8efc8b..6e02cab 100644 --- a/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor +++ b/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor @@ -1,16 +1,51 @@ @page "/inventory-catalog" @rendermode InteractiveServer - -@using CulinaryCommand.Inventory.DTOs +@implements IDisposable + +@using CulinaryCommandApp.Inventory.DTOs +@using CulinaryCommandApp.Inventory.Entities +@using CulinaryCommandApp.Inventory.Services +@using CulinaryCommandApp.Inventory.Services.Interfaces +@using CulinaryCommand.Services +@using CulinaryCommand.Services.UserContextSpace + +@inject IInventoryManagementService InventoryService +@inject IUnitService UnitService +@inject LocationState LocationState +@inject EnumService EnumService +@inject IUserContextService UserCtx +@inject NavigationManager Nav
+ @if (!_ready) + { +
Loading...
+ } + else if (!_allowed) + { +
+ You need Admin or Manager permissions to access the inventory catalog. +
+ } + else + {
-

Inventory Catalog - testing deployment change

-

Manage your ingredient catalog

+

Inventory Catalog

+

@(LocationState.CurrentLocation is not null ? $"{LocationState.CurrentLocation.Name} β€” ingredient catalog" : "Manage your ingredient catalog")

+ @if (LocationState.CurrentLocation is null) + { + + } + else + { +
+ + } @* end @if (LocationState.CurrentLocation is not null) *@ + + } @* end else (_allowed) *@ +
@@ -152,7 +192,7 @@