Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CulinaryCommandApp/Components/Custom/PrepTasksPanel.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@using CulinaryCommand.Data.Entities
@using CulinaryCommand.Data.Enums
@using Microsoft.AspNetCore.Components
@inject NavigationManager Nav

<div class="card shadow-sm border-0 cc-card">
<div class="cc-card-header">
Expand Down Expand Up @@ -208,6 +209,12 @@

private void OpenRecipe(Tasks t)
{
if (t.RecipeId.HasValue)
{
Nav.NavigateTo($"/recipes/view/{t.RecipeId.Value}?from=mytasks");
return;
}

selectedTask = t;
showRecipe = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ else
OnMarkInProgress="MarkInProgress"
OnMarkComplete="MarkComplete"
OnBumpTask="BumpTask"
OnDeleteTask="DeleteTask" />
OnDeleteTask="DeleteTask"
OnStartAllPending="StartAllPending" />
</div>
</div>
</div>
Expand Down Expand Up @@ -789,6 +790,18 @@ else

private Task MarkInProgress(int id) => MoveToStatus(id, WorkTaskStatus.InProgress);

private async Task StartAllPending()
{
var pendingTaskIds = TasksByStatus(WorkTaskStatus.Pending)
.Select(t => t.Id)
.ToList();

foreach (var taskId in pendingTaskIds)
{
await MarkInProgress(taskId);
}
}

private async Task BumpTask(int id)
{
await TaskService.BumpDueDateAsync(id, 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@
</span>
</div>

@if (status == WorkTaskStatus.Pending && tasks.Any())
{
<div class="mb-3">
<button class="btn btn-outline-primary btn-sm"
@onclick="HandleStartAllPending">
Start All Pending
</button>
</div>
}

<div style="max-height: 720px; overflow-y: auto;">
@if (!tasks.Any())
{
Expand Down Expand Up @@ -125,6 +135,7 @@
[Parameter] public EventCallback<int> OnMarkComplete { get; set; }
[Parameter] public EventCallback<int> OnBumpTask { get; set; }
[Parameter] public EventCallback<int> OnDeleteTask { get; set; }
[Parameter] public EventCallback OnStartAllPending { get; set; }

private string TaskBoardSearchValue
{
Expand All @@ -139,4 +150,6 @@
private Task HandleBumpTask(int taskId) => OnBumpTask.InvokeAsync(taskId);

private Task HandleDeleteTask(int taskId) => OnDeleteTask.InvokeAsync(taskId);

private Task HandleStartAllPending() => OnStartAllPending.InvokeAsync();
}
23 changes: 21 additions & 2 deletions CulinaryCommandApp/Components/Pages/FeedbackPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,26 @@
@inject IFeedbackService FeedbackSvc

<div class="feedback-page">
<h2>Feedback</h2>
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div class="page-heading" style="border-left: 4px solid #2ca259; padding-left: 1rem;">
<div class="text-uppercase text-muted small fw-semibold mb-1">Kitchen Operations</div>
<h1 class="fw-bold mb-1">Feedback</h1>
<p class="text-muted mb-0">Review submitted product feedback and follow up on issues.</p>
</div>

<div class="d-flex align-items-center gap-3">
<div class="d-flex gap-3">
<div>
<div class="text-muted small">Total</div>
<div class="fs-5 fw-semibold">@allFeedback.Count</div>
</div>
<div>
<div class="text-muted small">Filtered</div>
<div class="fs-5 fw-semibold">@Filtered.Count()</div>
</div>
</div>
</div>
</div>

<div class="feedback-filters">
<div class="filter-group">
Expand Down Expand Up @@ -162,4 +181,4 @@
"General" => "💬",
_ => ""
};
}
}
2 changes: 1 addition & 1 deletion CulinaryCommandApp/Components/Pages/Users/UserList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
@inject NavigationManager Nav

<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div>
<div class="page-heading" style="border-left: 4px solid #2ca259; padding-left: 1rem;">
<div class="text-uppercase text-muted small fw-semibold mb-1">Kitchen Operations</div>
<h1 class="fw-bold mb-1">Manage Users</h1>
<p class="text-muted mb-0">Invite teammates and manage access per location.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,31 @@
}
else
{
<div class="inventory-header">
<div class="header-text">
<h1>Inventory Catalog</h1>
<p>@(LocationState.CurrentLocation is not null ? $"{LocationState.CurrentLocation.Name} — ingredient catalog" : "Manage your ingredient catalog")</p>
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div class="page-heading" style="border-left: 4px solid #2ca259; padding-left: 1rem;">
<div class="text-uppercase text-muted small fw-semibold mb-1">Kitchen Operations</div>
<h1 class="fw-bold mb-1">Inventory Catalog</h1>
<p class="text-muted mb-0">Manage your ingredient catalog.</p>
</div>

<div class="d-flex align-items-center gap-3">
<div class="text-end">
<div class="small text-muted fw-semibold">Location</div>
<div class="fw-semibold">@(LocationState.CurrentLocation?.Name ?? "—")</div>
</div>

<div class="vr d-none d-md-block"></div>

<div class="d-flex gap-3">
<div>
<div class="text-muted small">Items</div>
<div class="fs-5 fw-semibold">@itemCatalog.Count</div>
</div>
<div>
<div class="text-muted small">Low stock</div>
<div class="fs-5 fw-semibold">@itemCatalog.Count(i => i.IsLowStock)</div>
</div>
</div>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@rendermode InteractiveServer

<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div>
<div class="page-heading" style="border-left: 4px solid #2ca259; padding-left: 1rem;">
<div class="text-uppercase text-muted small fw-semibold mb-1">Kitchen Operations</div>
<h1 class="fw-bold mb-1">Purchase Orders</h1>
<p class="text-muted mb-0">Create and track purchase orders for your location's suppliers.</p>
Expand Down
2 changes: 1 addition & 1 deletion CulinaryCommandApp/Recipe/Pages/RecipeList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ else

@* ── Header ── *@
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3 mb-4">
<div>
<div class="page-heading" style="border-left: 4px solid #2ca259; padding-left: 1rem;">
<div class="text-uppercase text-muted small fw-semibold mb-1">Kitchen Operations</div>
<h1 class="fw-bold mb-1">Recipes</h1>
<p class="text-muted mb-0">Manage your location's recipes and prep items.</p>
Expand Down
124 changes: 110 additions & 14 deletions CulinaryCommandApp/Recipe/Pages/RecipeView.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
@page "/recipes/view/{id:int}"
@rendermode InteractiveServer

@using CulinaryCommandApp.Inventory.DTOs
@using CulinaryCommandApp.Inventory.Services.Interfaces
@using CulinaryCommand.Services.UserContextSpace
@using CulinaryCommandApp.Recipe.Entities

@inject IRecipeService RecipeService
@inject IInventoryManagementService InventoryService
@inject IUserContextService UserCtx
@inject NavigationManager Nav

Expand Down Expand Up @@ -32,7 +35,7 @@

@* ── Back button ── *@
<div class="rv-back-row">
<a href="/recipes" class="rv-btn rv-btn-outline">
<a href="@BackHref" class="rv-btn rv-btn-outline">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
Expand Down Expand Up @@ -189,6 +192,30 @@
else
{
<span class="rv-ing-name">@(line.Ingredient?.Name ?? "—")</span>
@if (HasInventoryIngredient(line))
{
<div class="rv-stock-meta">
@if (IsOutOfStock(line))
{
<span class="rv-stock-state">Out of Stock</span>
<button class="rv-stock-action"
type="button"
@onclick="() => MarkInStockAsync(line)"
disabled="@IsUpdatingIngredient(line)">
Mark In Stock
</button>
}
else
{
<button class="rv-stock-action"
type="button"
@onclick="() => MarkOutOfStockAsync(line)"
disabled="@IsUpdatingIngredient(line)">
Mark Out of Stock
</button>
}
</div>
}
}
</td>
<td class="rv-prepnote">
Expand All @@ -198,7 +225,7 @@
@(line.Ingredient?.Vendor?.Name ?? "—")
</td>
<td class="col-inv">
@if (line.IngredientId.HasValue)
@if (HasInventoryIngredient(line))
{
<a href="/inventory" class="rv-inv-link" title="View in inventory">
<i class="bi bi-box-seam"></i>
Expand Down Expand Up @@ -272,13 +299,20 @@
@code {

[Parameter] public int Id { get; set; }
[SupplyParameterFromQuery(Name = "from")] public string? From { get; set; }

private Recipe? _model;
private bool _ready;
private bool _notFound;
private bool _priv;
private string? _sourceRestaurant;
private decimal _scaleServings = 1;
private int[] _allowedLocationIds = [];
private readonly HashSet<int> _updatingIngredientIds = new();
private string BackHref =>
string.Equals(From, "mytasks", StringComparison.OrdinalIgnoreCase)
? "/tasks"
: "/recipes";

protected override async Task OnInitializedAsync()
{
Expand All @@ -299,18 +333,8 @@
_priv = string.Equals(ctx.User.Role, "Admin", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ctx.User.Role, "Manager", StringComparison.OrdinalIgnoreCase);

var allowedLocationIds = ctx.AccessibleLocations.Select(l => l.Id);
_model = await RecipeService.GetByIdAsync(Id, allowedLocationIds);
_notFound = _model is null;

if (_model is not null)
{
_sourceRestaurant = ctx.AccessibleLocations
.FirstOrDefault(l => l.Id == _model.LocationId)?.Name;

// Default scale = original yield
_scaleServings = _model.YieldAmount ?? 1;
}
_allowedLocationIds = ctx.AccessibleLocations.Select(l => l.Id).ToArray();
await LoadRecipeAsync(ctx);

_ready = true;
}
Expand Down Expand Up @@ -338,4 +362,76 @@
var scaled = originalQty / baseYield.Value * _scaleServings;
return scaled.ToString("G29");
}

private bool HasInventoryIngredient(RecipeIngredient line) =>
!line.SubRecipeId.HasValue &&
line.IngredientId.HasValue &&
line.Ingredient is not null;

private bool IsUpdatingIngredient(RecipeIngredient line) =>
HasInventoryIngredient(line) &&
_updatingIngredientIds.Contains(line.Ingredient!.Id);

private bool IsOutOfStock(RecipeIngredient line) =>
HasInventoryIngredient(line) &&
line.Ingredient!.StockQuantity <= 0;

private async Task MarkOutOfStockAsync(RecipeIngredient line)
=> await SetIngredientStockAsync(line, 0);

private async Task MarkInStockAsync(RecipeIngredient line)
=> await SetIngredientStockAsync(line, 1);

private async Task SetIngredientStockAsync(RecipeIngredient line, decimal quantity)
{
if (!HasInventoryIngredient(line))
return;

var ingredient = line.Ingredient!;
if (!_updatingIngredientIds.Add(ingredient.Id))
return;

try
{
var currentScale = _scaleServings;

await InventoryService.UpdateItemAsync(new InventoryItemDTO
{
Id = ingredient.Id,
Name = ingredient.Name,
SKU = ingredient.Sku ?? string.Empty,
Category = ingredient.Category,
CurrentQuantity = quantity,
UnitId = ingredient.UnitId,
Price = ingredient.Price ?? 0m,
ReorderLevel = ingredient.ReorderLevel,
Notes = ingredient.Notes,
VendorId = ingredient.VendorId,
StorageLocationId = ingredient.StorageLocationId
});

ingredient.StockQuantity = quantity;
await LoadRecipeAsync();
_scaleServings = currentScale;
}
finally
{
_updatingIngredientIds.Remove(ingredient.Id);
}
}

private async Task LoadRecipeAsync(UserContext? ctx = null)
{
_model = await RecipeService.GetByIdAsync(Id, _allowedLocationIds);
_notFound = _model is null;

if (_model is null)
return;

ctx ??= await UserCtx.GetAsync();
_sourceRestaurant = ctx.AccessibleLocations
.FirstOrDefault(l => l.Id == _model.LocationId)?.Name;

_scaleServings = _model.YieldAmount ?? 1;
}
}
Loading
Loading