From 509a1e1c1f05dec1fc9722bd4801bb90f94f1849 Mon Sep 17 00:00:00 2001 From: Matayas Durr Date: Thu, 12 Mar 2026 17:10:24 -0500 Subject: [PATCH 1/2] Implement task library + task lists backend and bootstrap UI on assign tasks page --- .../Pages/Assignments/AdminAssignTask.razor | 693 ++++++-- CulinaryCommandApp/Data/AppDbContext.cs | 25 + CulinaryCommandApp/Data/Entities/TaskList.cs | 32 + .../Data/Entities/TaskListItem.cs | 18 + .../Data/Entities/TaskTemplate.cs | 57 + ...927_AddTaskLibraryAndTaskLists.Designer.cs | 1394 +++++++++++++++++ ...260312204927_AddTaskLibraryAndTaskLists.cs | 284 ++++ .../Migrations/AppDbContextModelSnapshot.cs | 244 ++- CulinaryCommandApp/Program.cs | 1 + .../Services/ITaskLibraryService.cs | 27 + .../Services/TaskLibraryService.cs | 149 ++ 11 files changed, 2738 insertions(+), 186 deletions(-) create mode 100644 CulinaryCommandApp/Data/Entities/TaskList.cs create mode 100644 CulinaryCommandApp/Data/Entities/TaskListItem.cs create mode 100644 CulinaryCommandApp/Data/Entities/TaskTemplate.cs create mode 100644 CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.Designer.cs create mode 100644 CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.cs create mode 100644 CulinaryCommandApp/Services/ITaskLibraryService.cs create mode 100644 CulinaryCommandApp/Services/TaskLibraryService.cs diff --git a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor index c8364e4..66f56a2 100644 --- a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor +++ b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor @@ -15,6 +15,7 @@ @inject ITaskAssignmentService TaskService @inject IRecipeService RecipeService @inject ITaskNotificationService TaskNotifier +@inject ITaskLibraryService TaskLibraryService @implements IDisposable @rendermode InteractiveServer @@ -33,218 +34,438 @@ else if (!allowed) } else { -
-
+
+
-

Plan the line, prep, and cleaning tasks for today.

-

Task Assignment

+
Kitchen Operations
+

Task Assignment

+

Manage task lists, assign library tasks fast, and track progress in one place.

-
-
-
Location
-
@SelectedLocationName
-
Change in header
+ +
+
+
Location
+
@SelectedLocationName
+
+ +
+ +
+
+
Open
+
@OpenTaskCount
+
+
+
Due today
+
@DueTodayCount
+
-
-
-
-
-
-
Create a task
-

Assign work to a team member with an expected due date.

+ +
+
+ + +
+
+
+
+
Task Lists
+
Reusable groups like Dish Closing or Morning Prep
+
+ @taskLists.Count +
+ +
+ + +
+ +
+ @if (!FilteredTaskLists.Any()) + { +
No task lists found.
+ } + else + { + @foreach (var list in FilteredTaskLists) + { +
+
+
+
@list.Name
+
@list.Description
+ + @(list.Items?.Count ?? 0) task@( (list.Items?.Count ?? 0) == 1 ? "" : "s") + +
+ + +
+
+ } + }
- Team: @teamMembers.Count
+
- - - - - -
- - - - - -
- Use "Prep from recipe" when you want it to show on the prep list with Par / Count. + +
+
+
+
+
Task Library
+
Select one or more reusable tasks
+ @taskTemplates.Count
-
- - +
+ +
-
-
- - - @foreach (var station in stationOptions) - { - - } - -
-
- - - @foreach (var p in priorityOptions) - { - - } - + @if (selectedTemplateIds.Any()) + { +
+ @selectedTemplateIds.Count task@(selectedTemplateIds.Count == 1 ? "" : "s") selected
+ } + +
+ @if (!FilteredTaskTemplates.Any()) + { +
No task templates found.
+ } + else + { + @foreach (var template in FilteredTaskTemplates) + { + var isSelected = selectedTemplateIds.Contains(template.Id); + +
+
+
+ + +
+
@template.Name
+
@template.Station
+ + @if (!string.IsNullOrWhiteSpace(template.Notes)) + { +
@template.Notes
+ } + +
+ @template.Priority + + @if (template.DefaultEstimatedMinutes.HasValue) + { + + @template.DefaultEstimatedMinutes min + + } +
+
+
+
+
+ } + }
+
+
- @* Prep-only fields *@ - @if (newTask.TaskType == WorkTaskKind.PrepFromRecipe) - { -
- - - - @foreach (var r in recipes) - { - - } + +
+ + Create Task Manually + Team: @teamMembers.Count + + +
+ + + + +
+ + + + -
- Choose the recipe this prep task is for. +
+ +
+ + +
+ +
+
+ + + @foreach (var station in stationOptions) + { + + } + +
+ +
+ + + @foreach (var p in priorityOptions) + { + + } +
+ @if (newTask.TaskType == WorkTaskKind.PrepFromRecipe) + { +
+ + + + @foreach (var r in recipes) + { + + } + +
+ +
+
+ + +
+
+ + +
+
+ } +
-
- - +
+ + + + @foreach (var user in teamMembers) + { + + } +
-
- - + +
+ +
-
- Prep amount will be calculated as max(Par − Count, 0) on the employee view. + +
+ +
- } -
-
- - - - @foreach (var user in teamMembers) + + +
+
+
+
+ + +
+
+ + + @if (SelectedTemplates.Any()) + { +
+
+
+
+
Quick Assign
+
+ @SelectedTemplates.Count task@(SelectedTemplates.Count == 1 ? "" : "s") selected +
+
+ + +
+ +
+
+ @foreach (var template in SelectedTemplates) { - + + @template.Name + } - +
-
- - + +
+
+ + + + @foreach (var user in teamMembers) + { + + } + +
+ +
+ + +
+ +
+ + + + @foreach (var p in priorityOptions) + { + + } + +
-
-
- - +
+ + +
+
+ } - - -
-
-
+ +
+
+
+
Kitchen Task Board
+
Track pending, active, and completed work
+
-
-
-
-
-
Open tasks
-
@OpenTaskCount
-
-
-
Due today
-
@DueTodayCount
-
-
-
- - +
+
+ + +
-
- -
- @foreach (var status in statusBuckets) - { -
-
-
-
-
@status
- @TasksByStatus(status).Count() -
- @if (!TasksByStatus(status).Any()) - { -
No tasks in this column.
- } - else - { - @foreach (var task in TasksByStatus(status)) - { -
-
-
-
@task.Name
-
@task.Station • @FormatAssignee(task.UserId)
-
Due @task.DueDate.ToString("MMM d")
-
- @task.Priority + +
+ @foreach (var status in statusBuckets) + { +
+
+
+
+
@status
+ + @TasksByStatus(status).Count() + +
+ +
+ @if (!TasksByStatus(status).Any()) + { +
+ No tasks in this column.
+ } + else + { + @foreach (var task in TasksByStatus(status)) + { +
+
+ @task.Priority +
+ +
@task.Name
+
@task.Station
+
@FormatAssignee(task.UserId)
+
Due @task.DueDate.ToString("MMM d")
-
- @if (task.Status != "Completed") - { - - } - @if (task.Status == "Pending") - { - - } - - -
-
- } - } + @if (!string.IsNullOrWhiteSpace(task.Notes)) + { +
@task.Notes
+ } + +
+ @if (task.Status == "Pending") + { + + } + + @if (task.Status != "Completed") + { + + } + + + + +
+
+ } + } +
+
-
- } + } +
@@ -262,6 +483,17 @@ else private string searchTerm = string.Empty; private List recipes = new(); + + private List taskTemplates = new(); + private List taskLists = new(); + + private List selectedTemplateIds = new(); + private string taskLibrarySearch = string.Empty; + private string taskListSearch = string.Empty; + + private int? quickAssignUserId; + private DateTime quickAssignDueDate = DateTime.Today; + private string quickAssignPriority = "Keep Original"; private readonly List stationOptions = new() { "Prep", "Grill", "Saute", "Expo", "Pastry", "Dish" }; private readonly List priorityOptions = new() { "Low", "Normal", "High", "Critical" }; @@ -306,13 +538,13 @@ else await LoadTeamAsync(); await LoadRecipesAsync(); await LoadAssignmentsAsync(); + await LoadTaskTemplatesAsync(); + await LoadTaskListsAsync(); ready = true; } - - private async Task LoadTeamAsync() { teamMembers.Clear(); @@ -365,6 +597,40 @@ else assignments = await TaskService.GetByLocationAsync(selectedLocationId.Value); } + private async Task LoadTaskTemplatesAsync() + { + taskTemplates.Clear(); + + if (!selectedLocationId.HasValue) + return; + + try + { + taskTemplates = await TaskLibraryService.GetTemplatesByLocationAsync(selectedLocationId.Value); + } + catch + { + taskTemplates = new(); + } + } + + private async Task LoadTaskListsAsync() + { + taskLists.Clear(); + + if (!selectedLocationId.HasValue) + return; + + try + { + taskLists = await TaskLibraryService.GetTaskListsByLocationAsync(selectedLocationId.Value); + } + catch + { + taskLists = new(); + } + } + private async Task HandleSubmit() { if (!selectedLocationId.HasValue) @@ -423,6 +689,34 @@ else await InvokeAsync(StateHasChanged); } + private async Task QuickAssignSelectedTemplates() + { + if (!selectedTemplateIds.Any()) + return; + + var assignerName = _ctx?.User?.Name ?? "Manager"; + + var createdTasks = await TaskLibraryService.AssignTemplatesAsync( + selectedTemplateIds, + quickAssignUserId, + quickAssignDueDate, + assignerName, + quickAssignPriority == "Keep Original" ? null : quickAssignPriority + ); + + if (createdTasks.Any()) + { + assignments.InsertRange(0, createdTasks); + } + + selectedTemplateIds.Clear(); + quickAssignUserId = null; + quickAssignDueDate = DateTime.Today; + quickAssignPriority = "Keep Original"; + + await InvokeAsync(StateHasChanged); + } + private IEnumerable TasksByStatus(string status) => assignments.Where(t => t.LocationId == (selectedLocationId ?? t.LocationId)) .Where(t => string.Equals(t.Status, status, StringComparison.OrdinalIgnoreCase)) @@ -483,10 +777,31 @@ else await LoadTeamAsync(); await LoadRecipesAsync(); await LoadAssignmentsAsync(); + await LoadTaskTemplatesAsync(); + await LoadTaskListsAsync(); StateHasChanged(); }); } + private void ToggleTemplateSelection(int templateId) + { + if (selectedTemplateIds.Contains(templateId)) + selectedTemplateIds.Remove(templateId); + else + selectedTemplateIds.Add(templateId); + } + + private async Task SelectTaskListAsync(int taskListId) + { + var templates = await TaskLibraryService.GetTemplatesForTaskListAsync(taskListId); + selectedTemplateIds = templates.Select(t => t.Id).ToList(); + } + + private void ClearSelectedTemplates() + { + selectedTemplateIds.Clear(); + } + private string SelectedLocationName => LocationState.CurrentLocation?.Name ?? "Select a location"; @@ -496,6 +811,22 @@ else private int OpenTaskCount => TasksForCurrentLocation.Count(t => t.Status != "Completed"); private int DueTodayCount => TasksForCurrentLocation.Count(t => t.DueDate.Date == DateTime.Today); + private IEnumerable FilteredTaskTemplates => + taskTemplates.Where(t => + string.IsNullOrWhiteSpace(taskLibrarySearch) || + t.Name.Contains(taskLibrarySearch, StringComparison.OrdinalIgnoreCase) || + t.Station.Contains(taskLibrarySearch, StringComparison.OrdinalIgnoreCase) || + (t.Notes?.Contains(taskLibrarySearch, StringComparison.OrdinalIgnoreCase) ?? false)); + + private IEnumerable FilteredTaskLists => + taskLists.Where(t => + string.IsNullOrWhiteSpace(taskListSearch) || + t.Name.Contains(taskListSearch, StringComparison.OrdinalIgnoreCase) || + (t.Description?.Contains(taskListSearch, StringComparison.OrdinalIgnoreCase) ?? false)); + + private List SelectedTemplates => + taskTemplates.Where(t => selectedTemplateIds.Contains(t.Id)).ToList(); + private string FormatAssignee(int? id) { if (!id.HasValue) return "Unassigned"; @@ -543,3 +874,25 @@ else public int? RecipeId { get; set; } } } + + + diff --git a/CulinaryCommandApp/Data/AppDbContext.cs b/CulinaryCommandApp/Data/AppDbContext.cs index e30d6a9..c151fd4 100644 --- a/CulinaryCommandApp/Data/AppDbContext.cs +++ b/CulinaryCommandApp/Data/AppDbContext.cs @@ -18,6 +18,9 @@ public AppDbContext(DbContextOptions options) public DbSet Locations => Set(); public DbSet Users => Set(); public DbSet Tasks => Set(); + public DbSet TaskTemplates => Set(); + public DbSet TaskLists => Set(); + public DbSet TaskListItems => Set(); public DbSet Companies => Set(); public DbSet Ingredients => Set(); public DbSet Recipes => Set(); @@ -141,6 +144,28 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.SetNull) .IsRequired(false); + // Configure relationship: each TaskListItem belongs to one TaskList, + // and each TaskList can contain many TaskListItems + modelBuilder.Entity() + .HasOne(x => x.TaskList) + .WithMany(x => x.Items) + .HasForeignKey(x => x.TaskListId) + .OnDelete(DeleteBehavior.Cascade); + + // Configure relationship: each TaskListItem points to one TaskTemplate, + // and each TaskTemplate can appear in many TaskListItems + modelBuilder.Entity() + .HasOne(x => x.TaskTemplate) + .WithMany(x => x.TaskListItems) + .HasForeignKey(x => x.TaskTemplateId) + .OnDelete(DeleteBehavior.Cascade); + + // Prevent duplicate entries so the same template can't be added + // to the same task list more than once + modelBuilder.Entity() + .HasIndex(x => new { x.TaskListId, x.TaskTemplateId }) + .IsUnique(); + // RecipeIngredient to parent Recipe modelBuilder.Entity() .HasOne(ri => ri.Recipe) diff --git a/CulinaryCommandApp/Data/Entities/TaskList.cs b/CulinaryCommandApp/Data/Entities/TaskList.cs new file mode 100644 index 0000000..86cacce --- /dev/null +++ b/CulinaryCommandApp/Data/Entities/TaskList.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace CulinaryCommand.Data.Entities +{ + public class TaskList + { + [Key] + public int Id { get; set; } + + [Required, MaxLength(256)] + public string Name { get; set; } = string.Empty; + + [MaxLength(512)] + public string? Description { get; set; } + + public int LocationId { get; set; } + public Location? Location { get; set; } + + public int? CreatedByUserId { get; set; } + public User? CreatedByUser { get; set; } + + public bool IsActive { get; set; } = true; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public ICollection Items { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/TaskListItem.cs b/CulinaryCommandApp/Data/Entities/TaskListItem.cs new file mode 100644 index 0000000..8e2a734 --- /dev/null +++ b/CulinaryCommandApp/Data/Entities/TaskListItem.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace CulinaryCommand.Data.Entities +{ + public class TaskListItem + { + [Key] + public int Id { get; set; } + + public int TaskListId { get; set; } + public TaskList? TaskList { get; set; } + + public int TaskTemplateId { get; set; } + public TaskTemplate? TaskTemplate { get; set; } + + public int SortOrder { get; set; } = 0; + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Entities/TaskTemplate.cs b/CulinaryCommandApp/Data/Entities/TaskTemplate.cs new file mode 100644 index 0000000..3584571 --- /dev/null +++ b/CulinaryCommandApp/Data/Entities/TaskTemplate.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using CulinaryCommand.Data.Enums; +using Rec = CulinaryCommandApp.Recipe.Entities; +using InvIngredient = CulinaryCommandApp.Inventory.Entities.Ingredient; + +namespace CulinaryCommand.Data.Entities +{ + public class TaskTemplate + { + [Key] + public int Id { get; set; } + + [Required, MaxLength(256)] + public string Name { get; set; } = string.Empty; + + [Required, MaxLength(128)] + public string Station { get; set; } = string.Empty; + + public WorkTaskKind Kind { get; set; } = WorkTaskKind.Generic; + + public string Priority { get; set; } = "Normal"; + public string? Notes { get; set; } + + public int? DefaultEstimatedMinutes { get; set; } + + public int LocationId { get; set; } + public Location? Location { get; set; } + + public int? CreatedByUserId { get; set; } + public User? CreatedByUser { get; set; } + + public bool IsActive { get; set; } = true; + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [Required] + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + public int? RecipeId { get; set; } + public Rec.Recipe? Recipe { get; set; } + + public int? IngredientId { get; set; } + public InvIngredient? Ingredient { get; set; } + + public int? Par { get; set; } + public int? Count { get; set; } + + [NotMapped] + public int Prep => (Par.HasValue && Count.HasValue) + ? Math.Max(Par.Value - Count.Value, 0) + : 0; + + public ICollection TaskListItems { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.Designer.cs b/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.Designer.cs new file mode 100644 index 0000000..2e1095a --- /dev/null +++ b/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.Designer.cs @@ -0,0 +1,1394 @@ +// +using System; +using CulinaryCommand.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CulinaryCommand.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260312204927_AddTaskLibraryAndTaskLists")] + partial class AddTaskLibraryAndTaskLists + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Company", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("City") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CompanyCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("LLCName") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Phone") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("State") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("TaxId") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ZipCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.HasKey("Id"); + + b.ToTable("Companies"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Device") + .HasColumnType("longtext"); + + b.Property("FeedbackType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Message") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Page") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ScreenshotBase64") + .HasColumnType("LONGTEXT"); + + b.Property("SubmittedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserEmail") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("UserRole") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Feedbacks"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Location", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("City") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("MarginEdgeKey") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ZipCode") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Locations"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.ManagerLocation", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.HasKey("UserId", "LocationId"); + + b.HasIndex("LocationId"); + + b.ToTable("ManagerLocations"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("LocationId"); + + b.ToTable("TaskLists"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TaskListId") + .HasColumnType("int"); + + b.Property("TaskTemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskTemplateId"); + + b.HasIndex("TaskListId", "TaskTemplateId") + .IsUnique(); + + b.ToTable("TaskListItems"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("DefaultEstimatedMinutes") + .HasColumnType("int"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Par") + .HasColumnType("int"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("Station") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("IngredientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("RecipeId"); + + b.ToTable("TaskTemplates"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Tasks", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Assigner") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DueDate") + .HasColumnType("datetime(6)"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Par") + .HasColumnType("int"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("Station") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("UserId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("InviteToken") + .HasColumnType("longtext"); + + b.Property("InviteTokenExpires") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Password") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Phone") + .HasColumnType("longtext"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("StationsWorked") + .HasColumnType("longtext"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.UserLocation", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Role") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.HasKey("UserId", "LocationId"); + + b.HasIndex("LocationId"); + + b.ToTable("UserLocations"); + }); + + modelBuilder.Entity("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("datetime(6)"); + + b.Property("IsLocationLocked") + .HasColumnType("tinyint(1)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OrderDate") + .HasColumnType("datetime(6)"); + + b.Property("OrderNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VendorContact") + .HasColumnType("longtext"); + + b.Property("VendorName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("OrderNumber") + .IsUnique(); + + b.ToTable("PurchaseOrders"); + }); + + modelBuilder.Entity("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrderLine", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("PurchaseOrderId") + .HasColumnType("int"); + + b.Property("QuantityOrdered") + .HasPrecision(18, 3) + .HasColumnType("decimal(18,3)"); + + b.Property("QuantityReceived") + .ValueGeneratedOnAdd() + .HasPrecision(18, 3) + .HasColumnType("decimal(18,3)") + .HasDefaultValue(0m); + + b.Property("UnitId") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("PurchaseOrderId"); + + b.HasIndex("UnitId"); + + b.ToTable("PurchaseOrderLines"); + }); + + modelBuilder.Entity("CulinaryCommand.Vendor.Entities.LocationVendor", b => + { + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("AssignedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("LocationId", "VendorId"); + + b.HasIndex("VendorId"); + + b.ToTable("LocationVendors"); + }); + + modelBuilder.Entity("CulinaryCommand.Vendor.Entities.Vendor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CompanyId") + .HasColumnType("int"); + + b.Property("ContactName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LogoUrl") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Phone") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Website") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("CompanyId"); + + b.ToTable("Vendors"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("IngredientId"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Price") + .HasColumnType("decimal(65,30)"); + + b.Property("ReorderLevel") + .HasColumnType("decimal(65,30)"); + + b.Property("Sku") + .HasColumnType("longtext"); + + b.Property("StockQuantity") + .HasColumnType("decimal(18, 4)"); + + b.Property("UnitId") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("LocationId"); + + b.HasIndex("UnitId"); + + b.HasIndex("VendorId"); + + b.ToTable("Ingredients", (string)null); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.InventoryTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("StockChange") + .HasColumnType("decimal(65,30)"); + + b.Property("UnitId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("IngredientId"); + + b.HasIndex("UnitId"); + + b.ToTable("InventoryTransactions"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.LocationUnit", b => + { + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("UnitId") + .HasColumnType("int"); + + b.Property("AssignedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("LocationId", "UnitId"); + + b.HasIndex("UnitId"); + + b.ToTable("LocationUnits"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.Unit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Abbreviation") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ConversionFactor") + .HasColumnType("decimal(65,30)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("Units"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.Recipe", b => + { + b.Property("RecipeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RecipeId")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CostPerYield") + .HasColumnType("decimal(65,30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsSubRecipe") + .HasColumnType("tinyint(1)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("RecipeType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("datetime(6)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("YieldAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("YieldUnit") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.HasKey("RecipeId"); + + b.HasIndex("LocationId"); + + b.ToTable("Recipes"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeIngredient", b => + { + b.Property("RecipeIngredientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RecipeIngredientId")); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("PrepNote") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Quantity") + .HasColumnType("decimal(65,30)"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("SubRecipeId") + .HasColumnType("int"); + + b.Property("UnitId") + .HasColumnType("int"); + + b.HasKey("RecipeIngredientId"); + + b.HasIndex("IngredientId"); + + b.HasIndex("RecipeId"); + + b.HasIndex("SubRecipeId"); + + b.HasIndex("UnitId"); + + b.ToTable("RecipeIngredients"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeStep", b => + { + b.Property("StepId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StepId")); + + b.Property("Duration") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("Equipment") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Instructions") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("StepNumber") + .HasColumnType("int"); + + b.Property("Temperature") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.HasKey("StepId"); + + b.HasIndex("RecipeId"); + + b.ToTable("RecipeSteps"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeSubRecipe", b => + { + b.Property("ParentRecipeId") + .HasColumnType("int"); + + b.Property("ChildRecipeId") + .HasColumnType("int"); + + b.HasKey("ParentRecipeId", "ChildRecipeId"); + + b.HasIndex("ChildRecipeId"); + + b.ToTable("RecipeSubRecipes"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Feedback", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Location", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Company", "Company") + .WithMany("Locations") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.ManagerLocation", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany("ManagerLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Data.Entities.User", "User") + .WithMany("ManagerLocations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId"); + + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskListItem", b => + { + b.HasOne("CulinaryCommand.Data.Entities.TaskList", "TaskList") + .WithMany("Items") + .HasForeignKey("TaskListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Data.Entities.TaskTemplate", "TaskTemplate") + .WithMany("TaskListItems") + .HasForeignKey("TaskTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskList"); + + b.Navigation("TaskTemplate"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId"); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId"); + + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId"); + + b.Navigation("CreatedByUser"); + + b.Navigation("Ingredient"); + + b.Navigation("Location"); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Tasks", b => + { + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId"); + + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CulinaryCommand.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Ingredient"); + + b.Navigation("Location"); + + b.Navigation("Recipe"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.User", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Company", "Company") + .WithMany("Employees") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.UserLocation", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany("UserLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Data.Entities.User", "User") + .WithMany("UserLocations") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrder", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrderLine", b => + { + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Lines") + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Unit", "Unit") + .WithMany() + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("CulinaryCommand.Vendor.Entities.LocationVendor", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany("LocationVendors") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Vendor.Entities.Vendor", "Vendor") + .WithMany("LocationVendors") + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("CulinaryCommand.Vendor.Entities.Vendor", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Company", "Company") + .WithMany("Vendors") + .HasForeignKey("CompanyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Company"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.Ingredient", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Unit", "Unit") + .WithMany("Ingredients") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Vendor.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Location"); + + b.Navigation("Unit"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.InventoryTransaction", b => + { + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Unit", "Unit") + .WithMany("InventoryTransaction") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.LocationUnit", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany("LocationUnits") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Unit", "Unit") + .WithMany("LocationUnits") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.Recipe", b => + { + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany("Recipes") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeIngredient", b => + { + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "Recipe") + .WithMany("RecipeIngredients") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "SubRecipe") + .WithMany() + .HasForeignKey("SubRecipeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Unit", "Unit") + .WithMany() + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Ingredient"); + + b.Navigation("Recipe"); + + b.Navigation("SubRecipe"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeStep", b => + { + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "Recipe") + .WithMany("Steps") + .HasForeignKey("RecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Recipe"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.RecipeSubRecipe", b => + { + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "ChildRecipe") + .WithMany("UsedInRecipes") + .HasForeignKey("ChildRecipeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "ParentRecipe") + .WithMany("SubRecipeUsages") + .HasForeignKey("ParentRecipeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildRecipe"); + + b.Navigation("ParentRecipe"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Company", b => + { + b.Navigation("Employees"); + + b.Navigation("Locations"); + + b.Navigation("Vendors"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Location", b => + { + b.Navigation("LocationUnits"); + + b.Navigation("LocationVendors"); + + b.Navigation("ManagerLocations"); + + b.Navigation("Recipes"); + + b.Navigation("UserLocations"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.Navigation("TaskListItems"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.User", b => + { + b.Navigation("ManagerLocations"); + + b.Navigation("UserLocations"); + }); + + modelBuilder.Entity("CulinaryCommand.PurchaseOrder.Entities.PurchaseOrder", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("CulinaryCommand.Vendor.Entities.Vendor", b => + { + b.Navigation("LocationVendors"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.Unit", b => + { + b.Navigation("Ingredients"); + + b.Navigation("InventoryTransaction"); + + b.Navigation("LocationUnits"); + }); + + modelBuilder.Entity("CulinaryCommandApp.Recipe.Entities.Recipe", b => + { + b.Navigation("RecipeIngredients"); + + b.Navigation("Steps"); + + b.Navigation("SubRecipeUsages"); + + b.Navigation("UsedInRecipes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.cs b/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.cs new file mode 100644 index 0000000..4e831f3 --- /dev/null +++ b/CulinaryCommandApp/Migrations/20260312204927_AddTaskLibraryAndTaskLists.cs @@ -0,0 +1,284 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CulinaryCommand.Migrations +{ + /// + public partial class AddTaskLibraryAndTaskLists : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "IsActive", + table: "Vendors", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(ulong), + oldType: "bit(1)"); + + migrationBuilder.AlterColumn( + name: "IsActive", + table: "Users", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(ulong), + oldType: "bit(1)"); + + migrationBuilder.AlterColumn( + name: "EmailConfirmed", + table: "Users", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(ulong), + oldType: "bit(1)"); + + migrationBuilder.AlterColumn( + name: "RowVersion", + table: "Recipes", + type: "datetime(6)", + rowVersion: true, + nullable: false, + oldClrType: typeof(DateTime), + oldType: "timestamp(6)", + oldRowVersion: true); + + migrationBuilder.AlterColumn( + name: "IsSubRecipe", + table: "Recipes", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(ulong), + oldType: "bit(1)"); + + migrationBuilder.AlterColumn( + name: "IsLocationLocked", + table: "PurchaseOrders", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(ulong), + oldType: "bit(1)"); + + migrationBuilder.CreateTable( + name: "TaskLists", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Description = table.Column(type: "varchar(512)", maxLength: 512, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + LocationId = table.Column(type: "int", nullable: false), + CreatedByUserId = table.Column(type: "int", nullable: true), + IsActive = table.Column(type: "tinyint(1)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskLists", x => x.Id); + table.ForeignKey( + name: "FK_TaskLists_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TaskLists_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TaskTemplates", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(256)", maxLength: 256, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Station = table.Column(type: "varchar(128)", maxLength: 128, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Kind = table.Column(type: "int", nullable: false), + Priority = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Notes = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + DefaultEstimatedMinutes = table.Column(type: "int", nullable: true), + LocationId = table.Column(type: "int", nullable: false), + CreatedByUserId = table.Column(type: "int", nullable: true), + IsActive = table.Column(type: "tinyint(1)", nullable: false), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + UpdatedAt = table.Column(type: "datetime(6)", nullable: false), + RecipeId = table.Column(type: "int", nullable: true), + IngredientId = table.Column(type: "int", nullable: true), + Par = table.Column(type: "int", nullable: true), + Count = table.Column(type: "int", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskTemplates", x => x.Id); + table.ForeignKey( + name: "FK_TaskTemplates_Ingredients_IngredientId", + column: x => x.IngredientId, + principalTable: "Ingredients", + principalColumn: "IngredientId"); + table.ForeignKey( + name: "FK_TaskTemplates_Locations_LocationId", + column: x => x.LocationId, + principalTable: "Locations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TaskTemplates_Recipes_RecipeId", + column: x => x.RecipeId, + principalTable: "Recipes", + principalColumn: "RecipeId"); + table.ForeignKey( + name: "FK_TaskTemplates_Users_CreatedByUserId", + column: x => x.CreatedByUserId, + principalTable: "Users", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "TaskListItems", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + TaskListId = table.Column(type: "int", nullable: false), + TaskTemplateId = table.Column(type: "int", nullable: false), + SortOrder = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TaskListItems", x => x.Id); + table.ForeignKey( + name: "FK_TaskListItems_TaskLists_TaskListId", + column: x => x.TaskListId, + principalTable: "TaskLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TaskListItems_TaskTemplates_TaskTemplateId", + column: x => x.TaskTemplateId, + principalTable: "TaskTemplates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_TaskListItems_TaskListId_TaskTemplateId", + table: "TaskListItems", + columns: new[] { "TaskListId", "TaskTemplateId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TaskListItems_TaskTemplateId", + table: "TaskListItems", + column: "TaskTemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskLists_CreatedByUserId", + table: "TaskLists", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskLists_LocationId", + table: "TaskLists", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskTemplates_CreatedByUserId", + table: "TaskTemplates", + column: "CreatedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskTemplates_IngredientId", + table: "TaskTemplates", + column: "IngredientId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskTemplates_LocationId", + table: "TaskTemplates", + column: "LocationId"); + + migrationBuilder.CreateIndex( + name: "IX_TaskTemplates_RecipeId", + table: "TaskTemplates", + column: "RecipeId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TaskListItems"); + + migrationBuilder.DropTable( + name: "TaskLists"); + + migrationBuilder.DropTable( + name: "TaskTemplates"); + + migrationBuilder.AlterColumn( + name: "IsActive", + table: "Vendors", + type: "bit(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + + migrationBuilder.AlterColumn( + name: "IsActive", + table: "Users", + type: "bit(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + + migrationBuilder.AlterColumn( + name: "EmailConfirmed", + table: "Users", + type: "bit(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + + migrationBuilder.AlterColumn( + name: "RowVersion", + table: "Recipes", + type: "timestamp(6)", + rowVersion: true, + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime(6)", + oldRowVersion: true); + + migrationBuilder.AlterColumn( + name: "IsSubRecipe", + table: "Recipes", + type: "bit(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + + migrationBuilder.AlterColumn( + name: "IsLocationLocked", + table: "PurchaseOrders", + type: "bit(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + } + } +} diff --git a/CulinaryCommandApp/Migrations/AppDbContextModelSnapshot.cs b/CulinaryCommandApp/Migrations/AppDbContextModelSnapshot.cs index aba897c..34344bc 100644 --- a/CulinaryCommandApp/Migrations/AppDbContextModelSnapshot.cs +++ b/CulinaryCommandApp/Migrations/AppDbContextModelSnapshot.cs @@ -190,6 +190,145 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ManagerLocations"); }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(512) + .HasColumnType("varchar(512)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("LocationId"); + + b.ToTable("TaskLists"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskListItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TaskListId") + .HasColumnType("int"); + + b.Property("TaskTemplateId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskTemplateId"); + + b.HasIndex("TaskListId", "TaskTemplateId") + .IsUnique(); + + b.ToTable("TaskListItems"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedByUserId") + .HasColumnType("int"); + + b.Property("DefaultEstimatedMinutes") + .HasColumnType("int"); + + b.Property("IngredientId") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Kind") + .HasColumnType("int"); + + b.Property("LocationId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Par") + .HasColumnType("int"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeId") + .HasColumnType("int"); + + b.Property("Station") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("IngredientId"); + + b.HasIndex("LocationId"); + + b.HasIndex("RecipeId"); + + b.ToTable("TaskTemplates"); + }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.Tasks", b => { b.Property("Id") @@ -292,7 +431,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar(256)"); b.Property("EmailConfirmed") - .HasColumnType("bit(1)"); + .HasColumnType("tinyint(1)"); b.Property("InviteToken") .HasColumnType("longtext"); @@ -301,7 +440,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("IsActive") - .HasColumnType("bit(1)"); + .HasColumnType("tinyint(1)"); b.Property("Name") .IsRequired() @@ -367,7 +506,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("IsLocationLocked") - .HasColumnType("bit(1)"); + .HasColumnType("tinyint(1)"); b.Property("LocationId") .HasColumnType("int"); @@ -490,7 +629,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar(256)"); b.Property("IsActive") - .HasColumnType("bit(1)"); + .HasColumnType("tinyint(1)"); b.Property("LogoUrl") .HasMaxLength(512) @@ -523,16 +662,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Vendors"); }); - - modelBuilder.Entity("CulinaryCommand.Data.Entities.Feedback", b => - { - b.HasOne("CulinaryCommand.Data.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId"); - - b.Navigation("User"); - }); - modelBuilder.Entity("CulinaryCommandApp.Inventory.Entities.Ingredient", b => { b.Property("Id") @@ -688,7 +817,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)"); b.Property("IsSubRecipe") - .HasColumnType("bit(1)"); + .HasColumnType("tinyint(1)"); b.Property("LocationId") .HasColumnType("int"); @@ -701,7 +830,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RowVersion") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() - .HasColumnType("timestamp(6)"); + .HasColumnType("datetime(6)"); b.Property("Title") .IsRequired() @@ -817,7 +946,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ChildRecipeId"); b.ToTable("RecipeSubRecipes"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.Feedback", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId"); + b.Navigation("User"); }); modelBuilder.Entity("CulinaryCommand.Data.Entities.Location", b => @@ -850,6 +987,71 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId"); + + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedByUser"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskListItem", b => + { + b.HasOne("CulinaryCommand.Data.Entities.TaskList", "TaskList") + .WithMany("Items") + .HasForeignKey("TaskListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommand.Data.Entities.TaskTemplate", "TaskTemplate") + .WithMany("TaskListItems") + .HasForeignKey("TaskTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskList"); + + b.Navigation("TaskTemplate"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.HasOne("CulinaryCommand.Data.Entities.User", "CreatedByUser") + .WithMany() + .HasForeignKey("CreatedByUserId"); + + b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") + .WithMany() + .HasForeignKey("IngredientId"); + + b.HasOne("CulinaryCommand.Data.Entities.Location", "Location") + .WithMany() + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CulinaryCommandApp.Recipe.Entities.Recipe", "Recipe") + .WithMany() + .HasForeignKey("RecipeId"); + + b.Navigation("CreatedByUser"); + + b.Navigation("Ingredient"); + + b.Navigation("Location"); + + b.Navigation("Recipe"); + }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.Tasks", b => { b.HasOne("CulinaryCommandApp.Inventory.Entities.Ingredient", "Ingredient") @@ -1137,6 +1339,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UserLocations"); }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskList", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("CulinaryCommand.Data.Entities.TaskTemplate", b => + { + b.Navigation("TaskListItems"); + }); + modelBuilder.Entity("CulinaryCommand.Data.Entities.User", b => { b.Navigation("ManagerLocations"); diff --git a/CulinaryCommandApp/Program.cs b/CulinaryCommandApp/Program.cs index 77cbb6d..3578ae5 100644 --- a/CulinaryCommandApp/Program.cs +++ b/CulinaryCommandApp/Program.cs @@ -165,6 +165,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/CulinaryCommandApp/Services/ITaskLibraryService.cs b/CulinaryCommandApp/Services/ITaskLibraryService.cs new file mode 100644 index 0000000..6f9b325 --- /dev/null +++ b/CulinaryCommandApp/Services/ITaskLibraryService.cs @@ -0,0 +1,27 @@ +using CulinaryCommand.Data.Entities; + +namespace CulinaryCommand.Services +{ + public interface ITaskLibraryService + { + Task> GetTemplatesByLocationAsync(int locationId); + Task> GetTaskListsByLocationAsync(int locationId); + Task> GetTemplatesForTaskListAsync(int taskListId); + + Task> AssignTemplatesAsync( + List taskTemplateIds, + int? userId, + DateTime dueDate, + string assigner, + string? priorityOverride = null + ); + + Task> AssignTaskListAsync( + int taskListId, + int? userId, + DateTime dueDate, + string assigner, + string? priorityOverride = null + ); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Services/TaskLibraryService.cs b/CulinaryCommandApp/Services/TaskLibraryService.cs new file mode 100644 index 0000000..b907f88 --- /dev/null +++ b/CulinaryCommandApp/Services/TaskLibraryService.cs @@ -0,0 +1,149 @@ +using CulinaryCommand.Data; +using CulinaryCommand.Data.Entities; +using CulinaryCommand.Data.Enums; +using Microsoft.EntityFrameworkCore; + +namespace CulinaryCommand.Services +{ + public class TaskLibraryService : ITaskLibraryService + { + private readonly AppDbContext _db; + + public TaskLibraryService(AppDbContext db) + { + _db = db; + } + + public async Task> GetTemplatesByLocationAsync(int locationId) + { + return await _db.TaskTemplates + .Where(t => t.LocationId == locationId && t.IsActive) + .OrderBy(t => t.Name) + .AsNoTracking() + .ToListAsync(); + } + + public async Task> GetTaskListsByLocationAsync(int locationId) + { + return await _db.TaskLists + .Where(tl => tl.LocationId == locationId && tl.IsActive) + .Include(tl => tl.Items) + .ThenInclude(i => i.TaskTemplate) + .OrderBy(tl => tl.Name) + .AsNoTracking() + .ToListAsync(); + } + + public async Task> GetTemplatesForTaskListAsync(int taskListId) + { + var taskList = await _db.TaskLists + .Include(tl => tl.Items.OrderBy(i => i.SortOrder)) + .ThenInclude(i => i.TaskTemplate) + .AsNoTracking() + .FirstOrDefaultAsync(tl => tl.Id == taskListId); + + if (taskList == null) + return new List(); + + return taskList.Items + .Where(i => i.TaskTemplate != null && i.TaskTemplate.IsActive) + .OrderBy(i => i.SortOrder) + .Select(i => i.TaskTemplate!) + .ToList(); + } + + public async Task> AssignTemplatesAsync( + List taskTemplateIds, + int? userId, + DateTime dueDate, + string assigner, + string? priorityOverride = null) + { + if (taskTemplateIds == null || !taskTemplateIds.Any()) + return new List(); + + var templates = await _db.TaskTemplates + .Where(t => taskTemplateIds.Contains(t.Id) && t.IsActive) + .OrderBy(t => t.Name) + .ToListAsync(); + + var createdTasks = new List(); + + foreach (var template in templates) + { + var task = BuildTaskFromTemplate(template, userId, dueDate, assigner, priorityOverride); + createdTasks.Add(task); + } + + _db.Tasks.AddRange(createdTasks); + await _db.SaveChangesAsync(); + + return createdTasks; + } + + public async Task> AssignTaskListAsync( + int taskListId, + int? userId, + DateTime dueDate, + string assigner, + string? priorityOverride = null) + { + var taskList = await _db.TaskLists + .Include(tl => tl.Items.OrderBy(i => i.SortOrder)) + .ThenInclude(i => i.TaskTemplate) + .FirstOrDefaultAsync(tl => tl.Id == taskListId && tl.IsActive); + + if (taskList == null) + return new List(); + + var createdTasks = new List(); + + foreach (var item in taskList.Items.OrderBy(i => i.SortOrder)) + { + if (item.TaskTemplate == null || !item.TaskTemplate.IsActive) + continue; + + var task = BuildTaskFromTemplate(item.TaskTemplate, userId, dueDate, assigner, priorityOverride); + createdTasks.Add(task); + } + + _db.Tasks.AddRange(createdTasks); + await _db.SaveChangesAsync(); + + return createdTasks; + } + + private static Tasks BuildTaskFromTemplate( + TaskTemplate template, + int? userId, + DateTime dueDate, + string assigner, + string? priorityOverride = null) + { + var finalPriority = !string.IsNullOrWhiteSpace(priorityOverride) + ? priorityOverride + : template.Priority; + + return new Tasks + { + Name = template.Name, + Station = template.Station, + Status = CulinaryCommand.Data.Enums.TaskStatus.Pending, + Assigner = assigner, + Date = DateTime.UtcNow, + UserId = userId, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + DueDate = dueDate, + LocationId = template.LocationId, + Kind = template.Kind, + RecipeId = template.RecipeId, + IngredientId = template.IngredientId, + Par = template.Par, + Count = template.Count, + Priority = finalPriority ?? "Normal", + Notes = template.Notes + }; + } + } +} \ No newline at end of file From b6e755f058859d6ce3dfa0ba14896e2ee37aa554 Mon Sep 17 00:00:00 2001 From: Matayas Durr Date: Thu, 26 Mar 2026 17:15:41 -0500 Subject: [PATCH 2/2] Implemented task template and task list management system with UI and backend support --- .../Pages/Assignments/AdminAssignTask.razor | 408 +++++++++++++++++- .../Assignments/CreateTaskListModal.razor | 80 ++++ .../Assignments/CreateTaskTemplateModal.razor | 134 ++++++ .../Pages/Assignments/EditTaskListModal.razor | 80 ++++ .../Assignments/EditTaskTemplateModal.razor | 136 ++++++ .../ManageTaskListTemplatesModal.razor | 128 ++++++ .../Data/Models/CreateTaskListRequest.cs | 20 + .../Data/Models/CreateTaskTemplateRequest.cs | 36 ++ .../Data/Models/UpdateTaskListRequest.cs | 21 + .../Data/Models/UpdateTaskTemplateRequest.cs | 39 ++ .../Services/ITaskLibraryService.cs | 11 + .../Services/TaskLibraryService.cs | 306 +++++++++++-- 12 files changed, 1358 insertions(+), 41 deletions(-) create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/CreateTaskListModal.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/CreateTaskTemplateModal.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/EditTaskListModal.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/EditTaskTemplateModal.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/ManageTaskListTemplatesModal.razor create mode 100644 CulinaryCommandApp/Data/Models/CreateTaskListRequest.cs create mode 100644 CulinaryCommandApp/Data/Models/CreateTaskTemplateRequest.cs create mode 100644 CulinaryCommandApp/Data/Models/UpdateTaskListRequest.cs create mode 100644 CulinaryCommandApp/Data/Models/UpdateTaskTemplateRequest.cs diff --git a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor index 66f56a2..65c78e8 100644 --- a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor +++ b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor @@ -7,6 +7,7 @@ @using CulinaryCommandApp.Recipe.Services; @using CulinaryCommandApp.Recipe.Services.Interfaces; @using CulinaryCommandApp.Recipe.Entities; +@using CulinaryCommand.Data.Models @inject IUserContextService UserCtx @inject NavigationManager Nav @inject ILocationService LocationService @@ -16,6 +17,7 @@ @inject IRecipeService RecipeService @inject ITaskNotificationService TaskNotifier @inject ITaskLibraryService TaskLibraryService +@inject IJSRuntime JS @implements IDisposable @rendermode InteractiveServer @@ -35,6 +37,19 @@ else if (!allowed) else {
+ @if (!string.IsNullOrWhiteSpace(createTemplateSuccess)) + { +
+ @createTemplateSuccess +
+ } + + @if (!string.IsNullOrWhiteSpace(createTemplateError)) + { +
+ @createTemplateError +
+ }
Kitchen Operations
@@ -76,15 +91,21 @@ else
Task Lists
Reusable groups like Dish Closing or Morning Prep
- @taskLists.Count + +
+ @taskLists.Count + +
+ placeholder="Search lists..." + @bind="taskListSearch" + @bind:event="oninput" />
@@ -100,16 +121,41 @@ else
@list.Name
-
@list.Description
+ + @if (!string.IsNullOrWhiteSpace(list.Description)) + { +
@list.Description
+ } + - @(list.Items?.Count ?? 0) task@( (list.Items?.Count ?? 0) == 1 ? "" : "s") + @(list.Items?.Count ?? 0) task@((list.Items?.Count ?? 0) == 1 ? "" : "s")
- +
+ + + + + + + +
} @@ -126,7 +172,13 @@ else
Task Library
Select one or more reusable tasks
- @taskTemplates.Count + +
+ @taskTemplates.Count + +
@@ -159,9 +211,9 @@ else
+ type="checkbox" + checked="@isSelected" + @onchange="() => ToggleTemplateSelection(template.Id)" />
@template.Name
@@ -184,7 +236,20 @@ else
+ + + +
+
} } @@ -472,6 +537,41 @@ else
} + + + + + + + + + + @code { private UserContext? _ctx; private bool ready; @@ -499,6 +599,19 @@ else private readonly List priorityOptions = new() { "Low", "Normal", "High", "Critical" }; private readonly List statusBuckets = new() { "Pending", "In Progress", "Completed" }; + private bool showCreateTemplateModal; + private string? createTemplateError; + private string? createTemplateSuccess; + private bool showEditTemplateModal; + private TaskTemplate? editingTemplate; + private bool showCreateTaskListModal; + private bool showEditTaskListModal; + private bool showManageTaskListTemplatesModal; + + private TaskList? editingTaskList; + private TaskList? managingTaskList; + + private List selectedTaskListTemplates = new(); protected override async Task OnInitializedAsync() { _ctx = await UserCtx.GetAsync(); @@ -802,6 +915,273 @@ else selectedTemplateIds.Clear(); } + private void OpenCreateTemplateModal() + { + createTemplateError = null; + createTemplateSuccess = null; + showCreateTemplateModal = true; + } + + private Task CloseCreateTemplateModal() + { + showCreateTemplateModal = false; + return Task.CompletedTask; + } + + private async Task SaveTemplate(CreateTaskTemplateRequest request) + { + createTemplateError = null; + createTemplateSuccess = null; + + try + { + var createdTemplate = await TaskLibraryService.CreateTemplateAsync(request); + + showCreateTemplateModal = false; + await LoadTaskTemplatesAsync(); + + createTemplateSuccess = $"Template '{createdTemplate.Name}' created successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private void OpenEditTemplateModal(TaskTemplate template) + { + createTemplateError = null; + createTemplateSuccess = null; + + editingTemplate = template; + showEditTemplateModal = true; + } + + private Task CloseEditTemplateModal() + { + showEditTemplateModal = false; + editingTemplate = null; + return Task.CompletedTask; + } + + private async Task SaveEditedTemplate(UpdateTaskTemplateRequest request) + { + createTemplateError = null; + createTemplateSuccess = null; + + try + { + var updatedTemplate = await TaskLibraryService.UpdateTemplateAsync(request); + + showEditTemplateModal = false; + editingTemplate = null; + + await LoadTaskTemplatesAsync(); + + createTemplateSuccess = $"Template '{updatedTemplate.Name}' updated successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task ArchiveTemplate(int templateId) + { + try + { + await TaskLibraryService.ArchiveTemplateAsync(templateId); + await LoadTaskTemplatesAsync(); + + createTemplateSuccess = "Template removed successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + } + + private async Task ConfirmArchive(int templateId) + { + var confirm = await JS.InvokeAsync("confirm", "Delete this template?"); + + if (confirm) + { + await ArchiveTemplate(templateId); + } + } + + private void OpenCreateTaskListModal() + { + createTemplateError = null; + createTemplateSuccess = null; + showCreateTaskListModal = true; + } + + private Task CloseCreateTaskListModal() + { + showCreateTaskListModal = false; + return Task.CompletedTask; + } + + private async Task SaveTaskList(CreateTaskListRequest request) + { + createTemplateError = null; + createTemplateSuccess = null; + + try + { + var createdTaskList = await TaskLibraryService.CreateTaskListAsync(request); + + showCreateTaskListModal = false; + await LoadTaskListsAsync(); + + createTemplateSuccess = $"Task list '{createdTaskList.Name}' created successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private void OpenEditTaskListModal(TaskList taskList) + { + createTemplateError = null; + createTemplateSuccess = null; + editingTaskList = taskList; + showEditTaskListModal = true; + } + + private Task CloseEditTaskListModal() + { + showEditTaskListModal = false; + editingTaskList = null; + return Task.CompletedTask; + } + + private async Task SaveEditedTaskList(UpdateTaskListRequest request) + { + createTemplateError = null; + createTemplateSuccess = null; + + try + { + var updatedTaskList = await TaskLibraryService.UpdateTaskListAsync(request); + + showEditTaskListModal = false; + editingTaskList = null; + + await LoadTaskListsAsync(); + + createTemplateSuccess = $"Task list '{updatedTaskList.Name}' updated successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task ConfirmArchiveTaskList(int taskListId) + { + var confirm = await JS.InvokeAsync("confirm", "Delete this task list?"); + if (confirm) + { + await ArchiveTaskList(taskListId); + } + } + + private async Task ArchiveTaskList(int taskListId) + { + createTemplateError = null; + createTemplateSuccess = null; + + try + { + await TaskLibraryService.ArchiveTaskListAsync(taskListId); + await LoadTaskListsAsync(); + + createTemplateSuccess = "Task list removed successfully."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task OpenManageTaskListTemplatesModal(TaskList taskList) + { + createTemplateError = null; + createTemplateSuccess = null; + + managingTaskList = taskList; + selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(taskList.Id); + showManageTaskListTemplatesModal = true; + } + + private Task CloseManageTaskListTemplatesModal() + { + showManageTaskListTemplatesModal = false; + managingTaskList = null; + selectedTaskListTemplates = new List(); + return Task.CompletedTask; + } + + private async Task AddTemplatesToManagingTaskList(List templateIds) + { + if (managingTaskList == null || !templateIds.Any()) + return; + + createTemplateError = null; + createTemplateSuccess = null; + + try + { + await TaskLibraryService.AddTemplatesToTaskListAsync(managingTaskList.Id, templateIds); + selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(managingTaskList.Id); + await LoadTaskListsAsync(); + + createTemplateSuccess = "Templates added to task list."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } + + private async Task RemoveTemplateFromManagingTaskList(int templateId) + { + if (managingTaskList == null) + return; + + createTemplateError = null; + createTemplateSuccess = null; + + try + { + await TaskLibraryService.RemoveTemplateFromTaskListAsync(managingTaskList.Id, templateId); + selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(managingTaskList.Id); + + createTemplateSuccess = "Template removed from task list."; + } + catch (Exception ex) + { + createTemplateError = ex.Message; + } + + await InvokeAsync(StateHasChanged); + } private string SelectedLocationName => LocationState.CurrentLocation?.Name ?? "Select a location"; diff --git a/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskListModal.razor b/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskListModal.razor new file mode 100644 index 0000000..8dac8a1 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskListModal.razor @@ -0,0 +1,80 @@ +@using CulinaryCommand.Data.Models +@using Microsoft.AspNetCore.Components.Forms + +@if (IsOpen) +{ + +} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public int LocationId { get; set; } + [Parameter] public int? CreatedByUserId { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnSave { get; set; } + + private CreateTaskListRequest Model = new(); + private bool IsSaving; + + protected override void OnParametersSet() + { + if (IsOpen) + { + Model = new CreateTaskListRequest + { + LocationId = LocationId, + CreatedByUserId = CreatedByUserId, + IsActive = true + }; + } + } + + private async Task HandleSubmit() + { + IsSaving = true; + Model.LocationId = LocationId; + Model.CreatedByUserId = CreatedByUserId; + await OnSave.InvokeAsync(Model); + IsSaving = false; + } + + private async Task Close() + { + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskTemplateModal.razor b/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskTemplateModal.razor new file mode 100644 index 0000000..357a721 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/CreateTaskTemplateModal.razor @@ -0,0 +1,134 @@ +@using CulinaryCommand.Data.Enums +@using CulinaryCommand.Data.Models +@using Microsoft.AspNetCore.Components.Forms + +@if (IsOpen) +{ + +} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public int LocationId { get; set; } + [Parameter] public int? CreatedByUserId { get; set; } + + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnSave { get; set; } + + private CreateTaskTemplateRequest Model = new(); + private bool IsSaving; + + private readonly List StationOptions = new() + { + "Prep", "Grill", "Saute", "Expo", "Pastry", "Dish" + }; + + private readonly List PriorityOptions = new() + { + "Low", "Normal", "High", "Critical" + }; + + protected override void OnParametersSet() + { + if (IsOpen) + { + Model = new CreateTaskTemplateRequest + { + LocationId = LocationId, + CreatedByUserId = CreatedByUserId, + Station = "Prep", + Priority = "Normal", + Kind = WorkTaskKind.Generic + }; + } + } + + private async Task HandleSubmit() + { + IsSaving = true; + + Model.LocationId = LocationId; + Model.CreatedByUserId = CreatedByUserId; + + await OnSave.InvokeAsync(Model); + + IsSaving = false; + } + + private async Task Close() + { + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Assignments/EditTaskListModal.razor b/CulinaryCommandApp/Components/Pages/Assignments/EditTaskListModal.razor new file mode 100644 index 0000000..bf520cc --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/EditTaskListModal.razor @@ -0,0 +1,80 @@ +@using CulinaryCommand.Data.Entities +@using CulinaryCommand.Data.Models +@using Microsoft.AspNetCore.Components.Forms + +@if (IsOpen && TaskList != null) +{ + +} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public TaskList? TaskList { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnSave { get; set; } + + private UpdateTaskListRequest Model = new(); + private bool IsSaving; + + protected override void OnParametersSet() + { + if (IsOpen && TaskList != null) + { + Model = new UpdateTaskListRequest + { + Id = TaskList.Id, + Name = TaskList.Name, + Description = TaskList.Description, + LocationId = TaskList.LocationId, + IsActive = TaskList.IsActive + }; + } + } + + private async Task HandleSubmit() + { + IsSaving = true; + await OnSave.InvokeAsync(Model); + IsSaving = false; + } + + private async Task Close() + { + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Assignments/EditTaskTemplateModal.razor b/CulinaryCommandApp/Components/Pages/Assignments/EditTaskTemplateModal.razor new file mode 100644 index 0000000..b852645 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/EditTaskTemplateModal.razor @@ -0,0 +1,136 @@ +@using CulinaryCommand.Data.Entities +@using CulinaryCommand.Data.Enums +@using CulinaryCommand.Data.Models +@using Microsoft.AspNetCore.Components.Forms + +@if (IsOpen && Template != null) +{ + +} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public TaskTemplate? Template { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + [Parameter] public EventCallback OnSave { get; set; } + + private UpdateTaskTemplateRequest Model = new(); + private bool IsSaving; + + private readonly List StationOptions = new() + { + "Prep", "Grill", "Saute", "Expo", "Pastry", "Dish" + }; + + private readonly List PriorityOptions = new() + { + "Low", "Normal", "High", "Critical" + }; + + protected override void OnParametersSet() + { + if (IsOpen && Template != null) + { + Model = new UpdateTaskTemplateRequest + { + Id = Template.Id, + Name = Template.Name, + Station = Template.Station, + Kind = Template.Kind, + Priority = Template.Priority, + Notes = Template.Notes, + DefaultEstimatedMinutes = Template.DefaultEstimatedMinutes, + LocationId = Template.LocationId, + RecipeId = Template.RecipeId, + IngredientId = Template.IngredientId, + Par = Template.Par, + Count = Template.Count, + IsActive = Template.IsActive + }; + } + } + + private async Task HandleSubmit() + { + IsSaving = true; + await OnSave.InvokeAsync(Model); + IsSaving = false; + } + + private async Task Close() + { + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Components/Pages/Assignments/ManageTaskListTemplatesModal.razor b/CulinaryCommandApp/Components/Pages/Assignments/ManageTaskListTemplatesModal.razor new file mode 100644 index 0000000..5fb3aea --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/ManageTaskListTemplatesModal.razor @@ -0,0 +1,128 @@ +@using CulinaryCommand.Data.Entities + +@if (IsOpen && TaskList != null) +{ + +} + +@code { + [Parameter] public bool IsOpen { get; set; } + [Parameter] public TaskList? TaskList { get; set; } + [Parameter] public List AvailableTemplates { get; set; } = new(); + [Parameter] public List CurrentTemplates { get; set; } = new(); + [Parameter] public EventCallback> OnAddTemplates { get; set; } + [Parameter] public EventCallback RemoveTemplate { get; set; } + [Parameter] public EventCallback OnClose { get; set; } + + private HashSet SelectedTemplateIds = new(); + private bool IsSaving; + + protected override void OnParametersSet() + { + if (IsOpen) + { + SelectedTemplateIds = new HashSet(); + } + } + + private void ToggleTemplate(int templateId) + { + if (!SelectedTemplateIds.Add(templateId)) + { + SelectedTemplateIds.Remove(templateId); + } + } + + private async Task SaveSelectedTemplates() + { + IsSaving = true; + await OnAddTemplates.InvokeAsync(SelectedTemplateIds.ToList()); + SelectedTemplateIds.Clear(); + IsSaving = false; + } + + private async Task Close() + { + await OnClose.InvokeAsync(); + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Models/CreateTaskListRequest.cs b/CulinaryCommandApp/Data/Models/CreateTaskListRequest.cs new file mode 100644 index 0000000..4d80c3a --- /dev/null +++ b/CulinaryCommandApp/Data/Models/CreateTaskListRequest.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace CulinaryCommand.Data.Models +{ + public class CreateTaskListRequest + { + [Required, StringLength(256)] + public string Name { get; set; } = string.Empty; + + [StringLength(1000)] + public string? Description { get; set; } + + [Required] + public int LocationId { get; set; } + + public int? CreatedByUserId { get; set; } + + public bool IsActive { get; set; } = true; + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Models/CreateTaskTemplateRequest.cs b/CulinaryCommandApp/Data/Models/CreateTaskTemplateRequest.cs new file mode 100644 index 0000000..3dfe88a --- /dev/null +++ b/CulinaryCommandApp/Data/Models/CreateTaskTemplateRequest.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; +using CulinaryCommand.Data.Enums; + +namespace CulinaryCommand.Data.Models +{ + public class CreateTaskTemplateRequest + { + [Required, StringLength(256)] + public string Name { get; set; } = string.Empty; + + [Required, StringLength(128)] + public string Station { get; set; } = "Prep"; + + public WorkTaskKind Kind { get; set; } = WorkTaskKind.Generic; + + [Required] + public string Priority { get; set; } = "Normal"; + + [StringLength(1000)] + public string? Notes { get; set; } + + [Range(1, 1440)] + public int? DefaultEstimatedMinutes { get; set; } + + [Required] + public int LocationId { get; set; } + + public int? CreatedByUserId { get; set; } + + public int? RecipeId { get; set; } + public int? IngredientId { get; set; } + + public int? Par { get; set; } + public int? Count { get; set; } + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Models/UpdateTaskListRequest.cs b/CulinaryCommandApp/Data/Models/UpdateTaskListRequest.cs new file mode 100644 index 0000000..0669ee5 --- /dev/null +++ b/CulinaryCommandApp/Data/Models/UpdateTaskListRequest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace CulinaryCommand.Data.Models +{ + public class UpdateTaskListRequest + { + [Required] + public int Id { get; set; } + + [Required, StringLength(256)] + public string Name { get; set; } = string.Empty; + + [StringLength(1000)] + public string? Description { get; set; } + + [Required] + public int LocationId { get; set; } + + public bool IsActive { get; set; } = true; + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Data/Models/UpdateTaskTemplateRequest.cs b/CulinaryCommandApp/Data/Models/UpdateTaskTemplateRequest.cs new file mode 100644 index 0000000..14e2bc6 --- /dev/null +++ b/CulinaryCommandApp/Data/Models/UpdateTaskTemplateRequest.cs @@ -0,0 +1,39 @@ +using System.ComponentModel.DataAnnotations; +using CulinaryCommand.Data.Enums; + +namespace CulinaryCommand.Data.Models +{ + public class UpdateTaskTemplateRequest + { + [Required] + public int Id { get; set; } + + [Required, StringLength(256)] + public string Name { get; set; } = string.Empty; + + [Required, StringLength(128)] + public string Station { get; set; } = "Prep"; + + public WorkTaskKind Kind { get; set; } = WorkTaskKind.Generic; + + [Required] + public string Priority { get; set; } = "Normal"; + + [StringLength(1000)] + public string? Notes { get; set; } + + [Range(1, 1440)] + public int? DefaultEstimatedMinutes { get; set; } + + [Required] + public int LocationId { get; set; } + + public int? RecipeId { get; set; } + public int? IngredientId { get; set; } + + public int? Par { get; set; } + public int? Count { get; set; } + + public bool IsActive { get; set; } = true; + } +} \ No newline at end of file diff --git a/CulinaryCommandApp/Services/ITaskLibraryService.cs b/CulinaryCommandApp/Services/ITaskLibraryService.cs index 6f9b325..9f8cf9b 100644 --- a/CulinaryCommandApp/Services/ITaskLibraryService.cs +++ b/CulinaryCommandApp/Services/ITaskLibraryService.cs @@ -1,4 +1,5 @@ using CulinaryCommand.Data.Entities; +using CulinaryCommand.Data.Models; namespace CulinaryCommand.Services { @@ -7,6 +8,16 @@ public interface ITaskLibraryService Task> GetTemplatesByLocationAsync(int locationId); Task> GetTaskListsByLocationAsync(int locationId); Task> GetTemplatesForTaskListAsync(int taskListId); + Task CreateTemplateAsync(CreateTaskTemplateRequest request); + Task UpdateTemplateAsync(UpdateTaskTemplateRequest request); + + Task ArchiveTemplateAsync(int templateId); + Task CreateTaskListAsync(CreateTaskListRequest request); + Task UpdateTaskListAsync(UpdateTaskListRequest request); + Task ArchiveTaskListAsync(int taskListId); + + Task AddTemplatesToTaskListAsync(int taskListId, List taskTemplateIds); + Task RemoveTemplateFromTaskListAsync(int taskListId, int taskTemplateId); Task> AssignTemplatesAsync( List taskTemplateIds, diff --git a/CulinaryCommandApp/Services/TaskLibraryService.cs b/CulinaryCommandApp/Services/TaskLibraryService.cs index b907f88..79f8b91 100644 --- a/CulinaryCommandApp/Services/TaskLibraryService.cs +++ b/CulinaryCommandApp/Services/TaskLibraryService.cs @@ -1,7 +1,8 @@ using CulinaryCommand.Data; using CulinaryCommand.Data.Entities; -using CulinaryCommand.Data.Enums; +using CulinaryCommand.Data.Models; using Microsoft.EntityFrameworkCore; +using CulinaryCommand.Data.Enums; namespace CulinaryCommand.Services { @@ -17,30 +18,31 @@ public TaskLibraryService(AppDbContext db) public async Task> GetTemplatesByLocationAsync(int locationId) { return await _db.TaskTemplates - .Where(t => t.LocationId == locationId && t.IsActive) - .OrderBy(t => t.Name) .AsNoTracking() + .Where(t => t.LocationId == locationId && t.IsActive) + .OrderBy(t => t.Station) + .ThenBy(t => t.Name) .ToListAsync(); } public async Task> GetTaskListsByLocationAsync(int locationId) { return await _db.TaskLists - .Where(tl => tl.LocationId == locationId && tl.IsActive) - .Include(tl => tl.Items) - .ThenInclude(i => i.TaskTemplate) - .OrderBy(tl => tl.Name) .AsNoTracking() + .Where(l => l.LocationId == locationId && l.IsActive) + .Include(l => l.Items) + .ThenInclude(i => i.TaskTemplate) + .OrderBy(l => l.Name) .ToListAsync(); } public async Task> GetTemplatesForTaskListAsync(int taskListId) { var taskList = await _db.TaskLists + .AsNoTracking() .Include(tl => tl.Items.OrderBy(i => i.SortOrder)) .ThenInclude(i => i.TaskTemplate) - .AsNoTracking() - .FirstOrDefaultAsync(tl => tl.Id == taskListId); + .FirstOrDefaultAsync(tl => tl.Id == taskListId && tl.IsActive); if (taskList == null) return new List(); @@ -52,6 +54,265 @@ public async Task> GetTemplatesForTaskListAsync(int taskListI .ToList(); } + public async Task CreateTemplateAsync(CreateTaskTemplateRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentException("Template name is required."); + + if (string.IsNullOrWhiteSpace(request.Station)) + throw new ArgumentException("Station is required."); + + if (request.LocationId <= 0) + throw new ArgumentException("A valid location is required."); + + var normalizedName = request.Name.Trim(); + var normalizedStation = request.Station.Trim(); + var normalizedPriority = string.IsNullOrWhiteSpace(request.Priority) + ? "Normal" + : request.Priority.Trim(); + + var duplicateExists = await _db.TaskTemplates.AnyAsync(t => + t.LocationId == request.LocationId && + t.IsActive && + t.Name == normalizedName && + t.Station == normalizedStation); + + if (duplicateExists) + throw new InvalidOperationException("A task template with this name and station already exists."); + + var template = new TaskTemplate + { + Name = normalizedName, + Station = normalizedStation, + Kind = request.Kind, + Priority = normalizedPriority, + Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(), + DefaultEstimatedMinutes = request.DefaultEstimatedMinutes, + LocationId = request.LocationId, + CreatedByUserId = request.CreatedByUserId, + RecipeId = request.RecipeId, + IngredientId = request.IngredientId, + Par = request.Par, + Count = request.Count, + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _db.TaskTemplates.Add(template); + await _db.SaveChangesAsync(); + + return template; + } + + public async Task UpdateTemplateAsync(UpdateTaskTemplateRequest request) + { + if (request.Id <= 0) + throw new ArgumentException("A valid template id is required."); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentException("Template name is required."); + + if (string.IsNullOrWhiteSpace(request.Station)) + throw new ArgumentException("Station is required."); + + var template = await _db.TaskTemplates.FirstOrDefaultAsync(t => t.Id == request.Id); + + if (template == null) + throw new InvalidOperationException("Task template not found."); + + var normalizedName = request.Name.Trim(); + var normalizedStation = request.Station.Trim(); + var normalizedPriority = string.IsNullOrWhiteSpace(request.Priority) + ? "Normal" + : request.Priority.Trim(); + + var duplicateExists = await _db.TaskTemplates.AnyAsync(t => + t.Id != request.Id && + t.LocationId == request.LocationId && + t.IsActive && + t.Name == normalizedName && + t.Station == normalizedStation); + + if (duplicateExists) + throw new InvalidOperationException("A task template with this name and station already exists."); + + template.Name = normalizedName; + template.Station = normalizedStation; + template.Kind = request.Kind; + template.Priority = normalizedPriority; + template.Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(); + template.DefaultEstimatedMinutes = request.DefaultEstimatedMinutes; + template.LocationId = request.LocationId; + template.RecipeId = request.RecipeId; + template.IngredientId = request.IngredientId; + template.Par = request.Par; + template.Count = request.Count; + template.IsActive = request.IsActive; + template.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + + return template; + } + + public async Task ArchiveTemplateAsync(int templateId) + { + var template = await _db.TaskTemplates.FirstOrDefaultAsync(t => t.Id == templateId); + + if (template == null) + throw new InvalidOperationException("Template not found."); + + template.IsActive = false; + template.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + } + + public async Task CreateTaskListAsync(CreateTaskListRequest request) + { + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentException("Task list name is required."); + + if (request.LocationId <= 0) + throw new ArgumentException("A valid location is required."); + + var normalizedName = request.Name.Trim(); + + var duplicateExists = await _db.TaskLists.AnyAsync(l => + l.LocationId == request.LocationId && + l.IsActive && + l.Name == normalizedName); + + if (duplicateExists) + throw new InvalidOperationException("A task list with this name already exists."); + + var taskList = new TaskList + { + Name = normalizedName, + Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(), + LocationId = request.LocationId, + CreatedByUserId = request.CreatedByUserId, + IsActive = request.IsActive, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + _db.TaskLists.Add(taskList); + await _db.SaveChangesAsync(); + + return taskList; + } + + public async Task UpdateTaskListAsync(UpdateTaskListRequest request) + { + if (request.Id <= 0) + throw new ArgumentException("A valid task list id is required."); + + if (string.IsNullOrWhiteSpace(request.Name)) + throw new ArgumentException("Task list name is required."); + + var taskList = await _db.TaskLists.FirstOrDefaultAsync(l => l.Id == request.Id); + + if (taskList == null) + throw new InvalidOperationException("Task list not found."); + + var normalizedName = request.Name.Trim(); + + var duplicateExists = await _db.TaskLists.AnyAsync(l => + l.Id != request.Id && + l.LocationId == request.LocationId && + l.IsActive && + l.Name == normalizedName); + + if (duplicateExists) + throw new InvalidOperationException("A task list with this name already exists."); + + taskList.Name = normalizedName; + taskList.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(); + taskList.LocationId = request.LocationId; + taskList.IsActive = request.IsActive; + taskList.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + + return taskList; + } + + public async Task ArchiveTaskListAsync(int taskListId) + { + var taskList = await _db.TaskLists.FirstOrDefaultAsync(l => l.Id == taskListId); + + if (taskList == null) + throw new InvalidOperationException("Task list not found."); + + taskList.IsActive = false; + taskList.UpdatedAt = DateTime.UtcNow; + + await _db.SaveChangesAsync(); + } + + public async Task AddTemplatesToTaskListAsync(int taskListId, List taskTemplateIds) + { + if (taskTemplateIds == null || !taskTemplateIds.Any()) + return; + + var taskList = await _db.TaskLists + .Include(l => l.Items) + .FirstOrDefaultAsync(l => l.Id == taskListId && l.IsActive); + + if (taskList == null) + throw new InvalidOperationException("Task list not found."); + + var validTemplateIds = await _db.TaskTemplates + .Where(t => taskTemplateIds.Contains(t.Id) && t.IsActive) + .Select(t => t.Id) + .ToListAsync(); + + var existingTemplateIds = taskList.Items + .Select(i => i.TaskTemplateId) + .ToHashSet(); + + var nextSortOrder = taskList.Items.Any() + ? taskList.Items.Max(i => i.SortOrder) + 1 + : 0; + + foreach (var templateId in validTemplateIds.Distinct()) + { + if (existingTemplateIds.Contains(templateId)) + continue; + + taskList.Items.Add(new TaskListItem + { + TaskListId = taskListId, + TaskTemplateId = templateId, + SortOrder = nextSortOrder++ + }); + } + + taskList.UpdatedAt = DateTime.UtcNow; + await _db.SaveChangesAsync(); + } + + public async Task RemoveTemplateFromTaskListAsync(int taskListId, int taskTemplateId) + { + var item = await _db.TaskListItems + .FirstOrDefaultAsync(i => i.TaskListId == taskListId && i.TaskTemplateId == taskTemplateId); + + if (item == null) + return; + + _db.TaskListItems.Remove(item); + + var taskList = await _db.TaskLists.FirstOrDefaultAsync(l => l.Id == taskListId); + if (taskList != null) + { + taskList.UpdatedAt = DateTime.UtcNow; + } + + await _db.SaveChangesAsync(); + } + public async Task> AssignTemplatesAsync( List taskTemplateIds, int? userId, @@ -67,13 +328,9 @@ public async Task> AssignTemplatesAsync( .OrderBy(t => t.Name) .ToListAsync(); - var createdTasks = new List(); - - foreach (var template in templates) - { - var task = BuildTaskFromTemplate(template, userId, dueDate, assigner, priorityOverride); - createdTasks.Add(task); - } + var createdTasks = templates + .Select(template => BuildTaskFromTemplate(template, userId, dueDate, assigner, priorityOverride)) + .ToList(); _db.Tasks.AddRange(createdTasks); await _db.SaveChangesAsync(); @@ -96,16 +353,11 @@ public async Task> AssignTaskListAsync( if (taskList == null) return new List(); - var createdTasks = new List(); - - foreach (var item in taskList.Items.OrderBy(i => i.SortOrder)) - { - if (item.TaskTemplate == null || !item.TaskTemplate.IsActive) - continue; - - var task = BuildTaskFromTemplate(item.TaskTemplate, userId, dueDate, assigner, priorityOverride); - createdTasks.Add(task); - } + var createdTasks = taskList.Items + .OrderBy(i => i.SortOrder) + .Where(i => i.TaskTemplate != null && i.TaskTemplate.IsActive) + .Select(i => BuildTaskFromTemplate(i.TaskTemplate!, userId, dueDate, assigner, priorityOverride)) + .ToList(); _db.Tasks.AddRange(createdTasks); await _db.SaveChangesAsync(); @@ -128,7 +380,7 @@ private static Tasks BuildTaskFromTemplate( { Name = template.Name, Station = template.Station, - Status = CulinaryCommand.Data.Enums.TaskStatus.Pending, + Status = Data.Enums.TaskStatus.Pending, Assigner = assigner, Date = DateTime.UtcNow, UserId = userId,