From 4cd1cf55c8fe2d9d001757dc3abe2751d40d3a6f Mon Sep 17 00:00:00 2001 From: Matayas Durr Date: Fri, 17 Apr 2026 14:40:17 -0500 Subject: [PATCH 1/2] Revert "Revert PR #141" This reverts commit d733c12b1d4e41e7d56680f582027fcd90aa3df7. --- .../Pages/Assignments/AdminAssignTask.razor | 868 +++++------------- .../Assignments/AdminAssignTask.razor.css | 52 ++ .../Pages/Assignments/ManualTaskForm.razor | 117 +++ .../Pages/Assignments/QuickAssignPanel.razor | 104 +++ .../Pages/Assignments/TaskBoardPanel.razor | 126 +++ .../Pages/Assignments/TaskFormModel.cs | 31 + .../Pages/Assignments/TaskLibraryPanel.razor | 120 +++ .../Pages/Assignments/TaskListsPanel.razor | 109 +++ 8 files changed, 879 insertions(+), 648 deletions(-) create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/ManualTaskForm.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/QuickAssignPanel.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskBoardPanel.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskFormModel.cs create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskLibraryPanel.razor create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskListsPanel.razor diff --git a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor index 65c78e8..cf5e1ec 100644 --- a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor +++ b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor @@ -3,15 +3,15 @@ @using CulinaryCommand.Data.Entities @using CulinaryCommand.Services @using CulinaryCommand.Data.Enums; +@using WorkTaskStatus = CulinaryCommand.Data.Enums.TaskStatus @using CulinaryCommand.Services.UserContextSpace -@using CulinaryCommandApp.Recipe.Services; @using CulinaryCommandApp.Recipe.Services.Interfaces; @using CulinaryCommandApp.Recipe.Entities; +@using CulinaryCommandApp.Components.Pages.Assignments @using CulinaryCommand.Data.Models @inject IUserContextService UserCtx @inject NavigationManager Nav @inject ILocationService LocationService -@inject IUserService UserService @inject LocationState LocationState @inject ITaskAssignmentService TaskService @inject IRecipeService RecipeService @@ -37,21 +37,21 @@ else if (!allowed) else {
- @if (!string.IsNullOrWhiteSpace(createTemplateSuccess)) - { -
- @createTemplateSuccess -
- } + @if (!string.IsNullOrWhiteSpace(pageSuccess)) + { +
+ @pageSuccess +
+ } - @if (!string.IsNullOrWhiteSpace(createTemplateError)) - { -
- @createTemplateError -
- } + @if (!string.IsNullOrWhiteSpace(pageError)) + { +
+ @pageError +
+ }
-
+
Kitchen Operations

Task Assignment

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

@@ -84,280 +84,36 @@ else
-
-
-
-
-
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
- - @if (!string.IsNullOrWhiteSpace(list.Description)) - { -
@list.Description
- } - - - @(list.Items?.Count ?? 0) task@((list.Items?.Count ?? 0) == 1 ? "" : "s") - -
- -
- - - - - - - -
-
-
- } - } -
-
-
+ -
-
-
-
-
Task Library
-
Select one or more reusable tasks
-
- -
- @taskTemplates.Count - -
-
- -
- - -
- - @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 - - } -
-
-
- - - - -
- -
- } - } -
-
-
+ -
- - Create Task Manually - Team: @teamMembers.Count - - -
- - - - -
- - - - - -
- -
- - -
- -
-
- - - @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) - { - - } - -
- -
- - -
-
- -
- - -
- - -
-
-
+
@@ -368,169 +124,30 @@ else @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
-
- -
-
- - -
-
-
-
- - -
- @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 (!string.IsNullOrWhiteSpace(task.Notes)) - { -
@task.Notes
- } - -
- @if (task.Status == "Pending") - { - - } - - @if (task.Status != "Completed") - { - - } - - - - -
-
- } - } -
-
-
-
- } -
+
@@ -580,38 +197,37 @@ else private List assignments = new(); private TaskFormModel newTask = new(); private int? selectedLocationId; - private string searchTerm = string.Empty; - + private string taskBoardSearch = 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" }; - 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(); + private string? pageError; + private string? pageSuccess; + + private readonly List stationOptions = new() { "Prep", "Grill", "Saute", "Expo", "Pastry", "Dish" }; + private readonly List priorityOptions = new() { "Low", "Normal", "High", "Critical" }; + private readonly List statusBuckets = new() + { + WorkTaskStatus.Pending, + WorkTaskStatus.InProgress, + WorkTaskStatus.Completed + }; + protected override async Task OnInitializedAsync() { _ctx = await UserCtx.GetAsync(); @@ -622,7 +238,6 @@ else return; } - // Invite-only: authenticated but not in DB if (_ctx.User is null) { Nav.NavigateTo("/no-access", true); @@ -639,25 +254,31 @@ else return; } - // Ensure LocationState is hydrated if user deep-links here if (LocationState.ManagedLocations.Count == 0 && _ctx.AccessibleLocations.Any()) await LocationState.SetLocationsAsync(_ctx.AccessibleLocations); - // Source of truth: header-selected location selectedLocationId = LocationState.CurrentLocation?.Id; - LocationState.OnChange += HandleLocationStateChanged; + await LoadPageDataAsync(); + + ready = true; + } + + public void Dispose() + { + LocationState.OnChange -= HandleLocationStateChanged; + } + + private async Task LoadPageDataAsync() + { await LoadTeamAsync(); await LoadRecipesAsync(); await LoadAssignmentsAsync(); await LoadTaskTemplatesAsync(); await LoadTaskListsAsync(); - - ready = true; } - private async Task LoadTeamAsync() { teamMembers.Clear(); @@ -670,12 +291,13 @@ else } catch { - // fallback sample users } } if (!teamMembers.Any()) { + // Demo fallback so the page still renders assignable users + // if location-based user lookup fails or returns empty. teamMembers = new() { new User { Id = 101, Name = "Ava Thompson", Role = "Prep Cook" }, @@ -706,7 +328,9 @@ else private async Task LoadAssignmentsAsync() { - if (!selectedLocationId.HasValue) return; + if (!selectedLocationId.HasValue) + return; + assignments = await TaskService.GetByLocationAsync(selectedLocationId.Value); } @@ -744,6 +368,16 @@ else } } + private void HandleLocationStateChanged() + { + _ = InvokeAsync(async () => + { + selectedLocationId = LocationState.CurrentLocation?.Id; + await LoadPageDataAsync(); + StateHasChanged(); + }); + } + private async Task HandleSubmit() { if (!selectedLocationId.HasValue) @@ -768,7 +402,7 @@ else Name = name, Station = newTask.Station, Priority = newTask.Priority, - Status = "Pending", + Status = WorkTaskStatus.Pending, UserId = newTask.UserId, DueDate = newTask.DueDate ?? DateTime.Today, Notes = newTask.Notes?.Trim(), @@ -777,10 +411,10 @@ else Kind = newTask.TaskType, Par = newTask.Par, Count = newTask.Count, - RecipeId = newTask.RecipeId, + RecipeId = newTask.RecipeId, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, - Date = DateTime.UtcNow // required DB field + Date = DateTime.UtcNow }; var created = await TaskService.CreateAsync(task); @@ -788,17 +422,14 @@ else newTask = new TaskFormModel { - TaskType = task.Kind, // keep last used type + TaskType = task.Kind, Station = task.Station, Priority = task.Priority, DueDate = task.DueDate, Notes = string.Empty - // RecipeId left null; choose fresh per task }; - // await TaskNotifier.NotifyTasksChangedAsync(selectedLocationId.Value); - await InvokeAsync(StateHasChanged); } @@ -814,8 +445,7 @@ else quickAssignUserId, quickAssignDueDate, assignerName, - quickAssignPriority == "Keep Original" ? null : quickAssignPriority - ); + quickAssignPriority == "Keep Original" ? null : quickAssignPriority); if (createdTasks.Any()) { @@ -830,72 +460,6 @@ else 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)) - .Where(FilterBySearch); - - private bool FilterBySearch(Tasks task) - { - if (string.IsNullOrWhiteSpace(searchTerm)) - return true; - - var term = searchTerm.Trim().ToLowerInvariant(); - return task.Name.ToLowerInvariant().Contains(term) - || task.Station.ToLowerInvariant().Contains(term) - || FormatAssignee(task.UserId).ToLowerInvariant().Contains(term); - } - - private async Task MoveToStatus(int id, string status) - { - await TaskService.UpdateStatusAsync(id, status); - - var t = assignments.FirstOrDefault(x => x.Id == id); - if (t == null) return; - - t.Status = status; - t.UpdatedAt = DateTime.UtcNow; - - await InvokeAsync(StateHasChanged); - } - - private Task MarkComplete(int id) => MoveToStatus(id, "Completed"); - private Task MarkInProgress(int id) => MoveToStatus(id, "In Progress"); - - private async Task BumpTask(int id) - { - await TaskService.BumpDueDateAsync(id, 1); - - var t = assignments.FirstOrDefault(x => x.Id == id); - if (t == null) return; - - t.DueDate = t.DueDate.AddDays(1); - t.UpdatedAt = DateTime.UtcNow; - - await InvokeAsync(StateHasChanged); - } - - private async Task DeleteTask(int id) - { - await TaskService.DeleteAsync(id); - assignments.RemoveAll(t => t.Id == id); - await InvokeAsync(StateHasChanged); - } - - private void HandleLocationStateChanged() - { - _ = InvokeAsync(async () => - { - selectedLocationId = LocationState.CurrentLocation?.Id; - await LoadTeamAsync(); - await LoadRecipesAsync(); - await LoadAssignmentsAsync(); - await LoadTaskTemplatesAsync(); - await LoadTaskListsAsync(); - StateHasChanged(); - }); - } - private void ToggleTemplateSelection(int templateId) { if (selectedTemplateIds.Contains(templateId)) @@ -917,8 +481,7 @@ else private void OpenCreateTemplateModal() { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); showCreateTemplateModal = true; } @@ -930,8 +493,7 @@ else private async Task SaveTemplate(CreateTaskTemplateRequest request) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { @@ -940,11 +502,11 @@ else showCreateTemplateModal = false; await LoadTaskTemplatesAsync(); - createTemplateSuccess = $"Template '{createdTemplate.Name}' created successfully."; + SetSuccess($"Template '{createdTemplate.Name}' created successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -952,9 +514,7 @@ else private void OpenEditTemplateModal(TaskTemplate template) { - createTemplateError = null; - createTemplateSuccess = null; - + ClearMessages(); editingTemplate = template; showEditTemplateModal = true; } @@ -968,8 +528,7 @@ else private async Task SaveEditedTemplate(UpdateTaskTemplateRequest request) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { @@ -977,14 +536,13 @@ else showEditTemplateModal = false; editingTemplate = null; - await LoadTaskTemplatesAsync(); - createTemplateSuccess = $"Template '{updatedTemplate.Name}' updated successfully."; + SetSuccess($"Template '{updatedTemplate.Name}' updated successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -996,20 +554,17 @@ else { await TaskLibraryService.ArchiveTemplateAsync(templateId); await LoadTaskTemplatesAsync(); - - createTemplateSuccess = "Template removed successfully."; + SetSuccess("Template removed successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } } private async Task ConfirmArchive(int templateId) { - var confirm = await JS.InvokeAsync("confirm", "Delete this template?"); - - if (confirm) + if (await ConfirmAsync("Delete this template?")) { await ArchiveTemplate(templateId); } @@ -1017,8 +572,7 @@ else private void OpenCreateTaskListModal() { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); showCreateTaskListModal = true; } @@ -1030,8 +584,7 @@ else private async Task SaveTaskList(CreateTaskListRequest request) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { @@ -1040,11 +593,11 @@ else showCreateTaskListModal = false; await LoadTaskListsAsync(); - createTemplateSuccess = $"Task list '{createdTaskList.Name}' created successfully."; + SetSuccess($"Task list '{createdTaskList.Name}' created successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -1052,8 +605,7 @@ else private void OpenEditTaskListModal(TaskList taskList) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); editingTaskList = taskList; showEditTaskListModal = true; } @@ -1067,8 +619,7 @@ else private async Task SaveEditedTaskList(UpdateTaskListRequest request) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { @@ -1076,14 +627,13 @@ else showEditTaskListModal = false; editingTaskList = null; - await LoadTaskListsAsync(); - createTemplateSuccess = $"Task list '{updatedTaskList.Name}' updated successfully."; + SetSuccess($"Task list '{updatedTaskList.Name}' updated successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -1091,8 +641,7 @@ else private async Task ConfirmArchiveTaskList(int taskListId) { - var confirm = await JS.InvokeAsync("confirm", "Delete this task list?"); - if (confirm) + if (await ConfirmAsync("Delete this task list?")) { await ArchiveTaskList(taskListId); } @@ -1100,19 +649,18 @@ else private async Task ArchiveTaskList(int taskListId) { - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { await TaskLibraryService.ArchiveTaskListAsync(taskListId); await LoadTaskListsAsync(); - createTemplateSuccess = "Task list removed successfully."; + SetSuccess("Task list removed successfully."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -1120,9 +668,7 @@ else private async Task OpenManageTaskListTemplatesModal(TaskList taskList) { - createTemplateError = null; - createTemplateSuccess = null; - + ClearMessages(); managingTaskList = taskList; selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(taskList.Id); showManageTaskListTemplatesModal = true; @@ -1141,8 +687,7 @@ else if (managingTaskList == null || !templateIds.Any()) return; - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { @@ -1150,11 +695,11 @@ else selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(managingTaskList.Id); await LoadTaskListsAsync(); - createTemplateSuccess = "Templates added to task list."; + SetSuccess("Templates added to task list."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } await InvokeAsync(StateHasChanged); @@ -1165,38 +710,94 @@ else if (managingTaskList == null) return; - createTemplateError = null; - createTemplateSuccess = null; + ClearMessages(); try { await TaskLibraryService.RemoveTemplateFromTaskListAsync(managingTaskList.Id, templateId); selectedTaskListTemplates = await TaskLibraryService.GetTemplatesForTaskListAsync(managingTaskList.Id); - createTemplateSuccess = "Template removed from task list."; + SetSuccess("Template removed from task list."); } catch (Exception ex) { - createTemplateError = ex.Message; + SetError(ex.Message); } 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)) + .Where(FilterBySearch); + + private bool FilterBySearch(Tasks task) + { + if (string.IsNullOrWhiteSpace(taskBoardSearch)) + return true; + + var term = taskBoardSearch.Trim().ToLowerInvariant(); + return (task.Name ?? string.Empty).ToLowerInvariant().Contains(term) + || (task.Station ?? string.Empty).ToLowerInvariant().Contains(term) + || FormatAssigneeName(task.UserId).ToLowerInvariant().Contains(term); + } + + private async Task MoveToStatus(int id, string status) + { + await TaskService.UpdateStatusAsync(id, status); + + var t = assignments.FirstOrDefault(x => x.Id == id); + if (t == null) + return; + + t.Status = status; + t.UpdatedAt = DateTime.UtcNow; + + await InvokeAsync(StateHasChanged); + } + + private Task MarkComplete(int id) => MoveToStatus(id, WorkTaskStatus.Completed); + + private Task MarkInProgress(int id) => MoveToStatus(id, WorkTaskStatus.InProgress); + + private async Task BumpTask(int id) + { + await TaskService.BumpDueDateAsync(id, 1); + + var t = assignments.FirstOrDefault(x => x.Id == id); + if (t == null) + return; + + t.DueDate = t.DueDate.AddDays(1); + t.UpdatedAt = DateTime.UtcNow; + + await InvokeAsync(StateHasChanged); + } + + private async Task DeleteTask(int id) + { + await TaskService.DeleteAsync(id); + assignments.RemoveAll(t => t.Id == id); + await InvokeAsync(StateHasChanged); + } + private string SelectedLocationName => LocationState.CurrentLocation?.Name ?? "Select a location"; private IEnumerable TasksForCurrentLocation => assignments.Where(t => !selectedLocationId.HasValue || t.LocationId == selectedLocationId.Value); - private int OpenTaskCount => TasksForCurrentLocation.Count(t => t.Status != "Completed"); + private int OpenTaskCount => TasksForCurrentLocation.Count(t => t.Status != WorkTaskStatus.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)); + 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 => @@ -1207,13 +808,15 @@ else private List SelectedTemplates => taskTemplates.Where(t => selectedTemplateIds.Contains(t.Id)).ToList(); - private string FormatAssignee(int? id) + private string FormatAssigneeName(int? id) { - if (!id.HasValue) return "Unassigned"; + if (!id.HasValue) + return "Unassigned"; + return teamMembers.FirstOrDefault(u => u.Id == id.Value)?.Name ?? "Unassigned"; } - private string PriorityBadge(string priority) => priority switch + private string GetPriorityBadgeClass(string priority) => priority switch { "Critical" => "badge bg-danger", "High" => "badge bg-warning text-dark", @@ -1222,57 +825,26 @@ else _ => "badge bg-secondary" }; - public void Dispose() + private void ClearMessages() { - LocationState.OnChange -= HandleLocationStateChanged; + pageError = null; + pageSuccess = null; } - private class TaskFormModel + private void SetSuccess(string message) { - [Required, StringLength(128)] - public string Name { get; set; } = string.Empty; - - [Required] - public string Station { get; set; } = "Prep"; - - [Required] - public string Priority { get; set; } = "Normal"; - - public int? UserId { get; set; } - - [Required] - public DateTime? DueDate { get; set; } = DateTime.Today; - - [StringLength(512)] - public string? Notes { get; set; } = string.Empty; - - public WorkTaskKind TaskType { get; set; } = WorkTaskKind.Generic; - - public int? Par { get; set; } - public int? Count { get; set; } - - public int? RecipeId { get; set; } - } -} - - - +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor.css b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor.css new file mode 100644 index 0000000..54205fd --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/AdminAssignTask.razor.css @@ -0,0 +1,52 @@ +.assign-task-page { + --assign-green: #009A3B; + --assign-green-dark: #007a2f; + --assign-green-soft: #2ca259; +} + +.assign-task-page .page-heading { + border-left: 4px solid var(--assign-green-soft); + padding-left: 1rem; +} + +.assign-task-page ::deep .card { + border-radius: 1.1rem; + border: 1px solid #e9ecef; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04); +} + +.assign-task-page ::deep .form-control, +.assign-task-page ::deep .form-select, +.assign-task-page ::deep .input-group-text, +.assign-task-page ::deep .btn { + border-radius: 0.9rem; +} + +.assign-task-page ::deep .btn-success, +.assign-task-page ::deep .btn-primary { + background-color: var(--assign-green); + border-color: var(--assign-green); +} + +.assign-task-page ::deep .btn-success:hover, +.assign-task-page ::deep .btn-primary:hover, +.assign-task-page ::deep .btn-success:focus, +.assign-task-page ::deep .btn-primary:focus { + background-color: var(--assign-green-dark); + border-color: var(--assign-green-dark); +} + +.assign-task-page ::deep .badge { + font-weight: 600; + border-radius: 999px; +} + +.assign-task-page ::deep .bg-success, +.assign-task-page ::deep .text-bg-success { + background-color: var(--assign-green) !important; + color: #ffffff !important; +} + +.assign-task-page ::deep details summary::-webkit-details-marker { + display: none; +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/ManualTaskForm.razor b/CulinaryCommandApp/Components/Pages/Assignments/ManualTaskForm.razor new file mode 100644 index 0000000..b4a3020 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/ManualTaskForm.razor @@ -0,0 +1,117 @@ +@using CulinaryCommand.Data.Entities +@using CulinaryCommand.Data.Enums +@using CulinaryCommandApp.Components.Pages.Assignments +@using CulinaryCommandApp.Recipe.Entities +@using Microsoft.AspNetCore.Components.Forms + +
+ + Create Task Manually + Team: @TeamMembers.Count + + +
+ + + + +
+ + + + + +
+ +
+ + +
+ +
+
+ + + @foreach (var station in StationOptions) + { + + } + +
+ +
+ + + @foreach (var priority in PriorityOptions) + { + + } + +
+
+ + @if (Model.TaskType == WorkTaskKind.PrepFromRecipe) + { +
+ + + + @foreach (var recipe in Recipes) + { + + } + +
+ +
+
+ + +
+
+ + +
+
+ } + +
+
+ + + + @foreach (var user in TeamMembers) + { + + } + +
+ +
+ + +
+
+ +
+ + +
+ + +
+
+
+ +@code { + [Parameter] public TaskFormModel Model { get; set; } = default!; + [Parameter] public List TeamMembers { get; set; } = new(); + [Parameter] public List Recipes { get; set; } = new(); + [Parameter] public List StationOptions { get; set; } = new(); + [Parameter] public List PriorityOptions { get; set; } = new(); + [Parameter] public EventCallback OnSubmit { get; set; } + + private Task HandleSubmit() => OnSubmit.InvokeAsync(); +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/QuickAssignPanel.razor b/CulinaryCommandApp/Components/Pages/Assignments/QuickAssignPanel.razor new file mode 100644 index 0000000..2d712c7 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/QuickAssignPanel.razor @@ -0,0 +1,104 @@ +@using CulinaryCommand.Data.Entities +@using Microsoft.AspNetCore.Components.Forms + +
+
+
+
+
Quick Assign
+
+ @SelectedTemplates.Count task@(SelectedTemplates.Count == 1 ? "" : "s") selected +
+
+ + +
+ +
+
+ @foreach (var template in SelectedTemplates) + { + + @template.Name + + } +
+
+ +
+
+ + + + @foreach (var user in TeamMembers) + { + + } + +
+ +
+ + +
+ +
+ + + + @foreach (var priority in PriorityOptions) + { + + } + +
+
+ +
+ + +
+
+
+ +@code { + [Parameter] public List SelectedTemplates { get; set; } = new(); + [Parameter] public List TeamMembers { get; set; } = new(); + [Parameter] public List PriorityOptions { get; set; } = new(); + [Parameter] public int? AssignedUserId { get; set; } + [Parameter] public EventCallback AssignedUserIdChanged { get; set; } + [Parameter] public DateTime DueDate { get; set; } + [Parameter] public EventCallback DueDateChanged { get; set; } + [Parameter] public string Priority { get; set; } = "Keep Original"; + [Parameter] public EventCallback PriorityChanged { get; set; } + [Parameter] public EventCallback OnAssign { get; set; } + [Parameter] public EventCallback OnClear { get; set; } + + private int? AssignedUserIdValue + { + get => AssignedUserId; + set => _ = AssignedUserIdChanged.InvokeAsync(value); + } + + private DateTime DueDateValue + { + get => DueDate; + set => _ = DueDateChanged.InvokeAsync(value); + } + + private string PriorityValue + { + get => Priority; + set => _ = PriorityChanged.InvokeAsync(value); + } + + private Task HandleAssign() => OnAssign.InvokeAsync(); + + private Task HandleClear() => OnClear.InvokeAsync(); +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/TaskBoardPanel.razor b/CulinaryCommandApp/Components/Pages/Assignments/TaskBoardPanel.razor new file mode 100644 index 0000000..98af6ee --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/TaskBoardPanel.razor @@ -0,0 +1,126 @@ +@using CulinaryCommand.Data.Entities +@using WorkTaskStatus = CulinaryCommand.Data.Enums.TaskStatus + +
+
+
+
Kitchen Task Board
+
Track pending, active, and completed work
+
+ +
+
+ + +
+
+
+
+ +
+ @foreach (var status in StatusBuckets) + { + var tasks = TasksForStatus(status).ToList(); + +
+
+
+
+
@status
+ + @tasks.Count + +
+ +
+ @if (!tasks.Any()) + { +
+ No tasks in this column. +
+ } + else + { + @foreach (var task in tasks) + { +
+
+ @task.Priority +
+ +
@task.Name
+
@task.Station
+
@AssigneeLabel(task.UserId)
+
Due @task.DueDate.ToString("MMM d")
+ + @if (!string.IsNullOrWhiteSpace(task.Notes)) + { +
@task.Notes
+ } + +
+ @if (task.Status == WorkTaskStatus.Pending) + { + + } + + @if (task.Status != WorkTaskStatus.Completed) + { + + } + + + + +
+
+ } + } +
+
+
+
+ } +
+ +@code { + [Parameter] public string TaskBoardSearch { get; set; } = string.Empty; + [Parameter] public EventCallback TaskBoardSearchChanged { get; set; } + [Parameter] public List StatusBuckets { get; set; } = new(); + [Parameter] public Func> TasksForStatus { get; set; } = default!; + [Parameter] public Func PriorityBadgeClass { get; set; } = default!; + [Parameter] public Func AssigneeLabel { get; set; } = default!; + [Parameter] public EventCallback OnMarkInProgress { get; set; } + [Parameter] public EventCallback OnMarkComplete { get; set; } + [Parameter] public EventCallback OnBumpTask { get; set; } + [Parameter] public EventCallback OnDeleteTask { get; set; } + + private string TaskBoardSearchValue + { + get => TaskBoardSearch; + set => _ = TaskBoardSearchChanged.InvokeAsync(value); + } + + private Task HandleMarkInProgress(int taskId) => OnMarkInProgress.InvokeAsync(taskId); + + private Task HandleMarkComplete(int taskId) => OnMarkComplete.InvokeAsync(taskId); + + private Task HandleBumpTask(int taskId) => OnBumpTask.InvokeAsync(taskId); + + private Task HandleDeleteTask(int taskId) => OnDeleteTask.InvokeAsync(taskId); +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/TaskFormModel.cs b/CulinaryCommandApp/Components/Pages/Assignments/TaskFormModel.cs new file mode 100644 index 0000000..7ef30df --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/TaskFormModel.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; +using CulinaryCommand.Data.Enums; + +namespace CulinaryCommandApp.Components.Pages.Assignments; + +public class TaskFormModel +{ + [Required, StringLength(128)] + public string Name { get; set; } = string.Empty; + + [Required] + public string Station { get; set; } = "Prep"; + + [Required] + public string Priority { get; set; } = "Normal"; + + public int? UserId { get; set; } + + [Required] + public DateTime? DueDate { get; set; } = DateTime.Today; + + [StringLength(512)] + public string? Notes { get; set; } = string.Empty; + + public WorkTaskKind TaskType { get; set; } = WorkTaskKind.Generic; + + public int? Par { get; set; } + public int? Count { get; set; } + + public int? RecipeId { get; set; } +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/TaskLibraryPanel.razor b/CulinaryCommandApp/Components/Pages/Assignments/TaskLibraryPanel.razor new file mode 100644 index 0000000..ea96d4d --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/TaskLibraryPanel.razor @@ -0,0 +1,120 @@ +@using CulinaryCommand.Data.Entities + +
+
+
+
+
Task Library
+
Select one or more reusable tasks
+
+ +
+ @TemplateCount + +
+
+ +
+ + +
+ + @if (SelectedTemplateIds.Any()) + { +
+ @SelectedTemplateIds.Count task@(SelectedTemplateIds.Count == 1 ? "" : "s") selected +
+ } + +
+ @if (!Templates.Any()) + { +
No task templates found.
+ } + else + { + @foreach (var template in Templates) + { + 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 + + } +
+
+
+ +
+ + +
+
+
+ } + } +
+
+
+ +@code { + [Parameter] public List Templates { get; set; } = new(); + [Parameter] public int TemplateCount { get; set; } + [Parameter] public string TaskLibrarySearch { get; set; } = string.Empty; + [Parameter] public EventCallback TaskLibrarySearchChanged { get; set; } + [Parameter] public IReadOnlyCollection SelectedTemplateIds { get; set; } = Array.Empty(); + [Parameter] public Func PriorityBadgeClass { get; set; } = default!; + [Parameter] public EventCallback OnToggleSelection { get; set; } + [Parameter] public EventCallback OnCreateTemplate { get; set; } + [Parameter] public EventCallback OnEditTemplate { get; set; } + [Parameter] public EventCallback OnArchiveTemplate { get; set; } + + private string TaskLibrarySearchValue + { + get => TaskLibrarySearch; + set => _ = TaskLibrarySearchChanged.InvokeAsync(value); + } + + private Task HandleToggleSelection(int templateId) => OnToggleSelection.InvokeAsync(templateId); + + private Task HandleCreateTemplate() => OnCreateTemplate.InvokeAsync(); + + private Task HandleEditTemplate(TaskTemplate template) => OnEditTemplate.InvokeAsync(template); + + private Task HandleArchiveTemplate(int templateId) => OnArchiveTemplate.InvokeAsync(templateId); +} diff --git a/CulinaryCommandApp/Components/Pages/Assignments/TaskListsPanel.razor b/CulinaryCommandApp/Components/Pages/Assignments/TaskListsPanel.razor new file mode 100644 index 0000000..dd9b22f --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/Assignments/TaskListsPanel.razor @@ -0,0 +1,109 @@ +@using CulinaryCommand.Data.Entities + +
+
+
+
+
Task Lists
+
Reusable groups like Dish Closing or Morning Prep
+
+ +
+ @TaskListCount + +
+
+ +
+ + +
+ +
+ @if (!TaskLists.Any()) + { +
No task lists found.
+ } + else + { + @foreach (var list in TaskLists) + { +
+
+
+
@list.Name
+ + @if (!string.IsNullOrWhiteSpace(list.Description)) + { +
@list.Description
+ } + + + @(list.Items?.Count ?? 0) task@((list.Items?.Count ?? 0) == 1 ? "" : "s") + +
+ +
+ + + + + + + +
+
+
+ } + } +
+
+
+ +@code { + [Parameter] public List TaskLists { get; set; } = new(); + [Parameter] public int TaskListCount { get; set; } + [Parameter] public string TaskListSearch { get; set; } = string.Empty; + [Parameter] public EventCallback TaskListSearchChanged { get; set; } + [Parameter] public EventCallback OnAssignList { get; set; } + [Parameter] public EventCallback OnCreateTaskList { get; set; } + [Parameter] public EventCallback OnEditTaskList { get; set; } + [Parameter] public EventCallback OnManageTemplates { get; set; } + [Parameter] public EventCallback OnArchiveTaskList { get; set; } + + private string TaskListSearchValue + { + get => TaskListSearch; + set => _ = TaskListSearchChanged.InvokeAsync(value); + } + + private Task HandleAssignList(int taskListId) => OnAssignList.InvokeAsync(taskListId); + + private Task HandleCreateTaskList() => OnCreateTaskList.InvokeAsync(); + + private Task HandleEditTaskList(TaskList taskList) => OnEditTaskList.InvokeAsync(taskList); + + private Task HandleManageTemplates(TaskList taskList) => OnManageTemplates.InvokeAsync(taskList); + + private Task HandleArchiveTaskList(int taskListId) => OnArchiveTaskList.InvokeAsync(taskListId); +} From 19efc44c54a0c5e49f2b8db05c42f96e29121651 Mon Sep 17 00:00:00 2001 From: Matayas Durr Date: Fri, 17 Apr 2026 14:40:17 -0500 Subject: [PATCH 2/2] Reapply "Merge pull request #142 from Culinary-Command/feature/assign-task-improvements" This reverts commit c8c1626852385404aa1803a6ebda148c6510c2ff. --- .../Components/Custom/PrepTasksPanel.razor | 87 ++++- .../Custom/PrepTasksPanel.razor.css | 360 +++++++++++------- .../Components/Layout/NavMenu.razor | 25 +- .../Pages/Assignments/AdminAssignTask.razor | 32 +- .../Assignments/AdminAssignTask.razor.css | 5 +- .../Assignments/CreateTaskListModal.razor | 121 +++++- .../Assignments/CreateTaskListModal.razor.css | 75 ++++ .../Assignments/CreateTaskTemplateModal.razor | 68 +++- .../CreateTaskTemplateModal.razor.css | 35 ++ .../Assignments/EditTaskTemplateModal.razor | 68 +++- .../EditTaskTemplateModal.razor.css | 35 ++ .../ManageTaskListTemplatesModal.razor | 163 +++++--- .../ManageTaskListTemplatesModal.razor.css | 72 ++++ .../Pages/Assignments/ManualTaskForm.razor | 41 +- .../Pages/Assignments/MyTasks.razor | 44 ++- .../Pages/Assignments/MyTasks.razor.css | 21 + .../Pages/Assignments/TaskBoardPanel.razor | 42 +- .../Assignments/TaskBoardPanel.razor.css | 23 ++ .../Pages/Assignments/TaskFormModel.cs | 25 +- .../Pages/Assignments/TaskLibraryPanel.razor | 35 +- .../Assignments/TaskLibraryPanel.razor.css | 23 ++ .../Pages/Assignments/TaskListsPanel.razor | 55 ++- .../Assignments/TaskListsPanel.razor.css | 23 ++ .../Data/Models/CreateTaskListRequest.cs | 4 +- .../Data/Models/CreateTaskTemplateRequest.cs | 27 +- .../Data/Models/UpdateTaskTemplateRequest.cs | 27 +- .../Services/TaskLibraryService.cs | 103 ++++- 27 files changed, 1280 insertions(+), 359 deletions(-) create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/CreateTaskListModal.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/CreateTaskTemplateModal.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/EditTaskTemplateModal.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/ManageTaskListTemplatesModal.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/MyTasks.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskBoardPanel.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskLibraryPanel.razor.css create mode 100644 CulinaryCommandApp/Components/Pages/Assignments/TaskListsPanel.razor.css diff --git a/CulinaryCommandApp/Components/Custom/PrepTasksPanel.razor b/CulinaryCommandApp/Components/Custom/PrepTasksPanel.razor index 7480fff..6216723 100644 --- a/CulinaryCommandApp/Components/Custom/PrepTasksPanel.razor +++ b/CulinaryCommandApp/Components/Custom/PrepTasksPanel.razor @@ -1,10 +1,16 @@ @namespace CulinaryCommand.Components.Custom @using CulinaryCommand.Data.Entities +@using CulinaryCommand.Data.Enums @using Microsoft.AspNetCore.Components -
+
-

Today's Prep Tasks

+
+ +

Today’s Task List

+

Review assigned work, expand general task notes, and complete prep with recipe context where needed.

+
+ @Tasks.Count
@if (Tasks is null || Tasks.Count == 0) @@ -26,29 +32,55 @@
-
- @DisplayTitle(t) +
+
+ @DisplayTitle(t) +
+ + @(IsPrepTask(t) ? "Recipe Prep" : "General") +
-
- Par: @(t.Par?.ToString() ?? "-") - Count: @(t.Count?.ToString() ?? "-") - Prep: @t.Prep + @if (IsPrepTask(t)) + { +
+ Par: @(t.Par?.ToString() ?? "0") + Count: @(t.Count?.ToString() ?? "0") + Prep: @t.Prep +
+ } + +
+ @if (ShouldShowRecipeDetails(t)) + { + + } + + @if (!IsPrepTask(t) && !string.IsNullOrWhiteSpace(t.Notes)) + { + + }
- @if (t.Recipe is not null) + @if (!IsPrepTask(t) && IsExpanded(t.Id) && !string.IsNullOrWhiteSpace(t.Notes)) { - +
+ @t.Notes +
}
- @t.DueDate.ToString("MMM d · h:mm tt") + @FormatAssignedDate(t)
-