From 859e5784e6898ad31a09c8046778d9c8ac7cef94 Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:09:40 -0500 Subject: [PATCH 1/6] Authentication Error Fix --- CulinaryCommandApp/Program.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CulinaryCommandApp/Program.cs b/CulinaryCommandApp/Program.cs index eee80e5..884e58f 100644 --- a/CulinaryCommandApp/Program.cs +++ b/CulinaryCommandApp/Program.cs @@ -48,6 +48,11 @@ .AddCookie() .AddOpenIdConnect(options => { + options.CorrelationCookie.SameSite = SameSiteMode.None; + options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.NonceCookie.SameSite = SameSiteMode.None; + options.NonceCookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; // ---- Read Cognito config (env/appsettings) ---- From 18f687ea068de642bb55adfa25de62cd97ba6d41 Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Mon, 23 Mar 2026 10:34:49 -0500 Subject: [PATCH 2/6] Add Inventory Config tab and unit manager Introduce an Inventory Config tab in Settings and add a new SettingsInventoryConfigurations component to manage units of measurement. The new component provides a form for creating/updating units (with validation), a table listing existing units, and delete confirmation modal. Access is restricted to Admin/Manager roles via IUserContextService; it uses IUnitService for CRUD operations and handles loading, save/delete states and basic error feedback. Settings.razor was updated to include the nav item and route case to render the new component. --- .../Pages/UserSettings/Settings.razor | 10 + .../SettingsInventoryConfigurations.razor | 321 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor index 46cdc20..bfc9b14 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor @@ -57,6 +57,12 @@ Company + @@ -87,6 +93,10 @@ break; + case "inventory-config": + + break; + default: break; diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor new file mode 100644 index 0000000..097c010 --- /dev/null +++ b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor @@ -0,0 +1,321 @@ +@rendermode InteractiveServer + +@using CulinaryCommand.Services.UserContextSpace +@using CulinaryCommandApp.Inventory.Entities +@using CulinaryCommandApp.Inventory.Services.Interfaces +@using System.ComponentModel.DataAnnotations + +@inject IUnitService UnitService +@inject IUserContextService UserCtx +@inject NavigationManager Nav + +
+

+ Inventory Configurations +

+ + @if (!_ready) + { +
+
+ Loading... +
+
+ } + else if (!_allowed) + { +
+ + You do not have permission to manage inventory configurations. Manager or Admin role required. +
+ } + else + { + +
+
+ Unit of Measurement +
+

Define standard units for ingredients and recipes in your kitchen

+ + + + +
+
+ + +
Full name of the measurement unit
+ +
+
+ + +
Short form for display
+ +
+
+ + +
Multiplier relative to base unit (e.g. 1000 for kg if gram is base)
+
+
+ + @if (!string.IsNullOrEmpty(_saveError)) + { +
+ @_saveError +
+ } + +
+ @if (_editingId.HasValue) + { + + + } + else + { + + + } +
+
+
+ + +
+
+ Existing Units +
+

Units that have been set and are usable across ingredients and recipes

+ + @if (!_units.Any()) + { +
+ + No units configured yet. Add one above to get started. +
+ } + else + { +
+ + + + + + + + + + + @foreach (var unit in _units) + { + + + + + + + } + +
Unit NameAbbreviationConv. FactorActions
@unit.Name@unit.Abbreviation@unit.ConversionFactor + + +
+
+
+ @_units.Count unit(s) configured +
+ } +
+ } +
+ + +@if (_unitToDelete is not null) +{ + +} + +@code { + private UserContext? _ctx; + private bool _ready; + private bool _allowed; + private bool _saving; + private bool _deleting; + private int? _editingId; + private string? _saveError; + private Unit? _unitToDelete; + private List _units = new(); + + private UnitFormModel _form = new(); + + protected override async Task OnInitializedAsync() + { + _ctx = await UserCtx.GetAsync(); + + if (_ctx.IsAuthenticated != true || _ctx.User is null) + { + Nav.NavigateTo("/login", true); + return; + } + + var role = _ctx.User.Role ?? string.Empty; + _allowed = role.Equals("Admin", StringComparison.OrdinalIgnoreCase) + || role.Equals("Manager", StringComparison.OrdinalIgnoreCase); + + if (_allowed) + _units = await UnitService.GetAllAsync(); + + _ready = true; + } + + private async Task HandleSave() + { + if (!_allowed || _saving) return; + + _saving = true; + _saveError = null; + + try + { + if (_editingId.HasValue) + { + var unit = new Unit + { + Id = _editingId.Value, + Name = _form.Name!, + Abbreviation = _form.Abbreviation!, + ConversionFactor = _form.ConversionFactor + }; + await UnitService.UpdateAsync(unit); + } + else + { + var unit = new Unit + { + Name = _form.Name!, + Abbreviation = _form.Abbreviation!, + ConversionFactor = _form.ConversionFactor + }; + await UnitService.CreateAsync(unit); + } + + _units = await UnitService.GetAllAsync(); + ResetForm(); + } + catch + { + _saveError = "An error occurred while saving. Please try again."; + } + finally + { + _saving = false; + } + } + + private void StartEdit(Unit unit) + { + _editingId = unit.Id; + _saveError = null; + _form = new UnitFormModel + { + Name = unit.Name, + Abbreviation = unit.Abbreviation, + ConversionFactor = unit.ConversionFactor + }; + } + + private void CancelEdit() => ResetForm(); + + private void ResetForm() + { + _form = new UnitFormModel(); + _editingId = null; + _saveError = null; + } + + private void ConfirmDelete(Unit unit) + { + _unitToDelete = unit; + } + + private void CancelDelete() + { + _unitToDelete = null; + } + + private async Task DeleteUnit() + { + if (_unitToDelete is null || _deleting) return; + + _deleting = true; + try + { + await UnitService.DeleteAsync(_unitToDelete.Id); + _units = await UnitService.GetAllAsync(); + + if (_editingId == _unitToDelete.Id) + ResetForm(); + + _unitToDelete = null; + } + finally + { + _deleting = false; + } + } + + private class UnitFormModel + { + [Required(ErrorMessage = "Unit name is required.")] + [StringLength(100, ErrorMessage = "Name must be 100 characters or fewer.")] + public string? Name { get; set; } + + [Required(ErrorMessage = "Abbreviation is required.")] + [StringLength(20, ErrorMessage = "Abbreviation must be 20 characters or fewer.")] + public string? Abbreviation { get; set; } + + public decimal ConversionFactor { get; set; } = 1; + } +} From bb1ec13ed66cdf281121c771b224065773ba3d30 Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:53:39 -0500 Subject: [PATCH 3/6] Adjust settings layout, remove icons, guard null user Increase Settings panel max-width from 700px to 960px for a wider layout. Remove Bootstrap icon elements from the Inventory tab button and Inventory Configurations header. Harden SettingsInventoryConfigurations.razor to avoid a potential null-reference by only checking _ctx.IsAuthenticated before redirect, then verifying _ctx.User is not null before reading Role and loading units. --- .../Components/Pages/UserSettings/Settings.css | 2 +- .../Pages/UserSettings/Settings.razor | 2 +- .../SettingsInventoryConfigurations.razor | 17 ++++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.css b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.css index 1e23a8e..a35fe3f 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.css +++ b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.css @@ -13,7 +13,7 @@ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 40px; width: 100%; - max-width: 700px; + max-width: 960px; } .app-title { diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor index bfc9b14..12794b3 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor @@ -60,7 +60,7 @@ diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor index 097c010..2790653 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor @@ -11,7 +11,7 @@

- Inventory Configurations + Inventory Configurations

@if (!_ready) @@ -194,18 +194,21 @@ { _ctx = await UserCtx.GetAsync(); - if (_ctx.IsAuthenticated != true || _ctx.User is null) + if (_ctx.IsAuthenticated != true) { Nav.NavigateTo("/login", true); return; } - var role = _ctx.User.Role ?? string.Empty; - _allowed = role.Equals("Admin", StringComparison.OrdinalIgnoreCase) - || role.Equals("Manager", StringComparison.OrdinalIgnoreCase); + if (_ctx.User is not null) + { + var role = _ctx.User.Role ?? string.Empty; + _allowed = role.Equals("Admin", StringComparison.OrdinalIgnoreCase) + || role.Equals("Manager", StringComparison.OrdinalIgnoreCase); - if (_allowed) - _units = await UnitService.GetAllAsync(); + if (_allowed) + _units = await UnitService.GetAllAsync(); + } _ready = true; } From fd152250cb12de0acb27fa4cf1ad39724a447f0a Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Thu, 26 Mar 2026 13:04:10 -0500 Subject: [PATCH 4/6] Removing conversion field --- .../SettingsInventoryConfigurations.razor | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor index 2790653..be62a93 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor @@ -22,19 +22,12 @@
} - else if (!_allowed) - { -
- - You do not have permission to manage inventory configurations. Manager or Admin role required. -
- } else {
- Unit of Measurement + Unit of Measurement

Define standard units for ingredients and recipes in your kitchen

@@ -54,11 +47,11 @@
Short form for display
-
+ @*
Multiplier relative to base unit (e.g. 1000 for kg if gram is base)
-
+
*@ @if (!string.IsNullOrEmpty(_saveError)) @@ -202,12 +195,8 @@ if (_ctx.User is not null) { - var role = _ctx.User.Role ?? string.Empty; - _allowed = role.Equals("Admin", StringComparison.OrdinalIgnoreCase) - || role.Equals("Manager", StringComparison.OrdinalIgnoreCase); - - if (_allowed) - _units = await UnitService.GetAllAsync(); + _allowed = true; + _units = await UnitService.GetAllAsync(); } _ready = true; From bd8ea0a3a5d5dfe027f786f42497191e20971c3e Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:04:45 -0500 Subject: [PATCH 5/6] Add Blazor.Bootstrap, Chart.js and scripts Integrate Blazor.Bootstrap and client-side libraries: App.razor now includes Bootstrap CSS/JS, bootstrap-icons, Blazor.Bootstrap CSS/JS, Chart.js (+ datalabels) and Sortable.js. CulinaryCommand.csproj adds the Blazor.Bootstrap package and _Imports.razor registers BlazorBootstrap. Program.cs registers AddBlazorBootstrap and changes the Google Client singleton to require a Google:ApiKey from configuration (throws if missing). Added a local alias for Unit in two Razor pages to resolve type usage. RecipeService validation for a required Category was removed to allow creating recipes without that check. --- CulinaryCommandApp/Components/App.razor | 11 +++++++++++ .../LocationSettings/ConfigureLocation.razor | 1 + .../SettingsInventoryConfigurations.razor | 1 + CulinaryCommandApp/Components/_Imports.razor | 3 ++- CulinaryCommandApp/CulinaryCommand.csproj | 1 + CulinaryCommandApp/Program.cs | 10 +++++++--- CulinaryCommandApp/Recipe/Services/RecipeService.cs | 3 --- 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CulinaryCommandApp/Components/App.razor b/CulinaryCommandApp/Components/App.razor index 88e8715..f4fb82d 100644 --- a/CulinaryCommandApp/Components/App.razor +++ b/CulinaryCommandApp/Components/App.razor @@ -8,6 +8,9 @@ + + + @@ -31,6 +34,14 @@ crossorigin="anonymous"> + + + + + + + + diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor b/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor index 7cea837..52ec2c0 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/LocationSettings/ConfigureLocation.razor @@ -6,6 +6,7 @@ @using CulinaryCommand.Services @using CulinaryCommand.Services.UserContextSpace @using CulinaryCommandApp.Inventory.Entities +@using Unit = CulinaryCommandApp.Inventory.Entities.Unit @using CulinaryCommandApp.Inventory.Services @using CulinaryCommandApp.Inventory.Services.Interfaces @using Microsoft.AspNetCore.Authorization diff --git a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor index be62a93..bb0d966 100644 --- a/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor +++ b/CulinaryCommandApp/Components/Pages/UserSettings/SettingsInventoryConfigurations.razor @@ -2,6 +2,7 @@ @using CulinaryCommand.Services.UserContextSpace @using CulinaryCommandApp.Inventory.Entities +@using Unit = CulinaryCommandApp.Inventory.Entities.Unit @using CulinaryCommandApp.Inventory.Services.Interfaces @using System.ComponentModel.DataAnnotations diff --git a/CulinaryCommandApp/Components/_Imports.razor b/CulinaryCommandApp/Components/_Imports.razor index 8306129..d126159 100644 --- a/CulinaryCommandApp/Components/_Imports.razor +++ b/CulinaryCommandApp/Components/_Imports.razor @@ -12,4 +12,5 @@ @using CulinaryCommand.Services @using CulinaryCommand.Components.Layout @using CulinaryCommand.Components.Custom -@using CulinaryCommand.Components.Pages \ No newline at end of file +@using CulinaryCommand.Components.Pages +@using BlazorBootstrap; \ No newline at end of file diff --git a/CulinaryCommandApp/CulinaryCommand.csproj b/CulinaryCommandApp/CulinaryCommand.csproj index 2989c24..52dd607 100644 --- a/CulinaryCommandApp/CulinaryCommand.csproj +++ b/CulinaryCommandApp/CulinaryCommand.csproj @@ -15,6 +15,7 @@ + diff --git a/CulinaryCommandApp/Program.cs b/CulinaryCommandApp/Program.cs index 20b8f15..6343192 100644 --- a/CulinaryCommandApp/Program.cs +++ b/CulinaryCommandApp/Program.cs @@ -35,7 +35,7 @@ // ===================== builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); - +builder.Services.AddBlazorBootstrap(); // // ===================== // Cognito Authentication (MUST be before Build) @@ -122,8 +122,12 @@ // ===================== // AI Services // ===================== -builder.Services.AddSingleton(_ => new Client()); -builder.Services.AddScoped(); +builder.Services.AddSingleton(sp => +{ + var apiKey = builder.Configuration["Google:ApiKey"] + ?? throw new InvalidOperationException("Missing config: Google:ApiKey"); + return new Client(apiKey: apiKey); +});builder.Services.AddScoped(); // // ===================== diff --git a/CulinaryCommandApp/Recipe/Services/RecipeService.cs b/CulinaryCommandApp/Recipe/Services/RecipeService.cs index 0e063bc..5f84827 100644 --- a/CulinaryCommandApp/Recipe/Services/RecipeService.cs +++ b/CulinaryCommandApp/Recipe/Services/RecipeService.cs @@ -57,9 +57,6 @@ public RecipeService(AppDbContext db) public async Task CreateAsync(Rec.Recipe recipe) { - if (string.IsNullOrWhiteSpace(recipe.Category)) - throw new Exception("Category is required."); - _db.Recipes.Add(recipe); await _db.SaveChangesAsync(); } From 7bab7146b32e50b329ac8ab4da4e273e412cf388 Mon Sep 17 00:00:00 2001 From: Anthony Phan <131195703+antphan12@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:53:39 -0500 Subject: [PATCH 6/6] Dashboard Update Using Plotly Chart tool, create new cards/charts to display base information. Card 1: Display Low Stock Card 2: Count of Tasks Completed or Pending Card 3: Count of Tasks completed by employee name Card 4: Dynamic Activity card tracking live updates --- CulinaryCommandApp/Components/App.razor | 2 + .../Components/Pages/Dashboard.razor | 338 +++++++++++++++++- CulinaryCommandApp/Components/_Imports.razor | 8 +- CulinaryCommandApp/CulinaryCommand.csproj | 1 + CulinaryCommandApp/Program.cs | 3 +- 5 files changed, 338 insertions(+), 14 deletions(-) diff --git a/CulinaryCommandApp/Components/App.razor b/CulinaryCommandApp/Components/App.razor index f4fb82d..427815d 100644 --- a/CulinaryCommandApp/Components/App.razor +++ b/CulinaryCommandApp/Components/App.razor @@ -42,6 +42,8 @@ + + diff --git a/CulinaryCommandApp/Components/Pages/Dashboard.razor b/CulinaryCommandApp/Components/Pages/Dashboard.razor index a50c2ca..9e8e0f0 100644 --- a/CulinaryCommandApp/Components/Pages/Dashboard.razor +++ b/CulinaryCommandApp/Components/Pages/Dashboard.razor @@ -6,10 +6,20 @@ @using CulinaryCommandApp.AIDashboard.Services.Reporting @using CulinaryCommandApp.AIDashboard.Services.DTOs @using CulinaryCommand.Services +@using CulinaryCommandApp.Inventory.Services.Interfaces +@using CulinaryCommandApp.Inventory.Services +@using CulinaryCommandApp.Inventory.DTOs +@using CulinaryCommand.Data +@using CulinaryCommand.PurchaseOrder.Entities +@using Microsoft.EntityFrameworkCore @inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment Env +@inject AppDbContext Db @inject NavigationManager Nav @inject AIReportingService ReportingService @inject IUserContextService UserCtx +@inject IInventoryManagementService InventorySvc +@inject ITaskAssignmentService TaskSvc +@inject LocationState LocationState @if (!_ready) { @@ -18,7 +28,7 @@ else if (!_isSignedIn) {
- You’re not signed in. + You're not signed in.
} @@ -40,6 +50,148 @@ else
+ +
+ + +
+
+
Low Stock Items (@_lowStockItems.Count)
+
+ @if (_lowStockItems.Any()) + { +
+ + + + + + + + + + + + + @foreach (var item in _lowStockItems) + { + + + + + + + + + } + +
IngredientCategoryIn StockReorder LevelUnitVendor
@item.Name@item.Category@item.CurrentQuantity.ToString("0.##")@item.ReorderLevel.ToString("0.##")@item.Unit@(item.VendorName ?? "—")
+
+ } + else + { +

All inventory items are sufficiently stocked.

+ } +
+
+
+ + +
+
+
Task Status Breakdown
+
+ @if (_taskData.Any()) + { + + } + else + { +

No tasks for this location.

+ } +
+
+
+
+ + +
+
+
+
Tasks Completed per Employee
+
+ @if (_employeeTaskCounts.Any()) + { + + + + + + + + + @foreach (var e in _employeeTaskCounts) + { + + + + + } + +
EmployeeCompleted
@e.Name + @e.Count +
+ } + else + { +

No completed tasks yet.

+ } +
+
+
+
+ + + +
+
+ Recent Activity + Last 7 days +
+
+ @if (!_activityFeed.Any()) + { +

No recent activity.

+ } + else + { +
    + @foreach (var item in _activityFeed) + { +
  • + + + +
    +
    @item.Text
    + @if (!string.IsNullOrWhiteSpace(item.Who)) + { + @item.Who + } +
    + @TimeAgo(item.When) +
  • + } +
+ } +
+
+ +
+ +
Weekly Report Analysis
@@ -140,21 +292,55 @@ else } @code { + // ── Auth ───────────────────────────────────────────────────────── private bool _ready; private bool _isSignedIn; private string? _role; + private int? _locationId; + // ── AI ─────────────────────────────────────────────────────────── private bool _aiLoading; private string? _aiAnalysis; private AIAnalysisResultDTO? _aiAnalysisObj; private bool _aiLoadedOnce; + // ── Inventory ──────────────────────────────────────────────────── + private List _inventoryData = new(); + private List _lowStockItems = new(); + + // ── Activity feed ──────────────────────────────────────────────── + private record ActivityItem(string Icon, string BadgeClass, string Text, string? Who, DateTime When); + private List _activityFeed = new(); + + // ── Task chart ─────────────────────────────────────────────────── + private List _taskData = new(); + private IList _taskChartData = new List(); + private Layout _taskLayout = new(); + + // ── Employee task completion ────────────────────────────────────── + private record EmployeeCount(string Name, int Count); + private List _employeeTaskCounts = new(); + + // Shared config + private Config _chartConfig = new() { Responsive = true }; + + // ── Lifecycle ──────────────────────────────────────────────────── protected override async Task OnInitializedAsync() { var ctx = await UserCtx.GetAsync(); _isSignedIn = ctx.User?.Id != null; _role = ctx.User?.Role; + await LocationState.SetLocationsAsync(ctx.AccessibleLocations); + _locationId = LocationState.CurrentLocation?.Id; + + if (_isSignedIn && _locationId.HasValue) + { + await LoadInventoryChartAsync(_locationId.Value); + await LoadTaskChartAsync(_locationId.Value); + await LoadActivityFeedAsync(_locationId.Value); + } + _ready = true; } @@ -168,37 +354,165 @@ else _aiLoading = true; StateHasChanged(); - try{ - if (!string.IsNullOrWhiteSpace(_aiAnalysis)) + try + { + if (!string.IsNullOrWhiteSpace(_aiAnalysis)) { var csvPath = Path.Combine(Env.ContentRootPath, "AIDashboard", "Services", "Reporting", "test_data.csv"); _aiAnalysis = await ReportingService.AnalyzeCsvAsync(csvPath); - - // Try to deserialize the returned JSON into the DTO so we can render structured cards UI. + if (!string.IsNullOrWhiteSpace(_aiAnalysis)) { var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _aiAnalysisObj = System.Text.Json.JsonSerializer.Deserialize(_aiAnalysis, options); } - } + } } - catch(Exception e) + catch (Exception e) { Console.WriteLine($"AI Analysis failed: {e.Message}"); _aiAnalysisObj = null; _aiAnalysis = null; - } - finally { + finally + { _aiLoading = false; StateHasChanged(); } - _aiLoading = false; - //_aiAnalysisObj = null; // Uncoomment above and delete curent like to activate AI - StateHasChanged(); } } + // ── Data loaders ───────────────────────────────────────────────── + private async Task LoadInventoryChartAsync(int locationId) + { + _inventoryData = await InventorySvc.GetItemsByLocationAsync(locationId); + _lowStockItems = _inventoryData + .Where(i => i.IsLowStock) + .OrderBy(i => i.CurrentQuantity) + .ToList(); + } + + private async Task LoadTaskChartAsync(int locationId) + { + _taskData = await TaskSvc.GetByLocationAsync(locationId); + + var groups = _taskData + .GroupBy(t => string.IsNullOrWhiteSpace(t.Status) ? "Unknown" : t.Status) + .ToDictionary(g => g.Key, g => g.Count()); + + _taskChartData = new List + { + new Pie + { + Labels = groups.Keys.Select(k => (object)k).ToList(), + Values = groups.Values.Select(v => (object)v).ToList(), + Hole = (decimal?)0.4 + } + }; + + _taskLayout = new Layout + { + PaperBgColor = "transparent", + PlotBgColor = "transparent", + Margin = new Plotly.Blazor.LayoutLib.Margin { T = 10, B = 10, L = 10, R = 10 }, + ShowLegend = true + }; + + _employeeTaskCounts = _taskData + .Where(t => string.Equals(t.Status, "Completed", StringComparison.OrdinalIgnoreCase) + && t.User != null) + .GroupBy(t => t.User!.Name ?? "Unknown") + .Select(g => new EmployeeCount(g.Key, g.Count())) + .OrderByDescending(e => e.Count) + .ToList(); + + } + + private async Task LoadActivityFeedAsync(int locationId) + { + var cutoff = DateTime.UtcNow.AddDays(-7); + var items = new List(); + + // Completed tasks from already-loaded task data + var completedTasks = _taskData + .Where(t => string.Equals(t.Status, "Completed", StringComparison.OrdinalIgnoreCase) + && t.UpdatedAt >= cutoff) + .OrderByDescending(t => t.UpdatedAt) + .Take(10); + + foreach (var t in completedTasks) + { + var who = t.User?.Name ?? "Someone"; + items.Add(new ActivityItem( + Icon: "bi-check-circle-fill", + BadgeClass: "text-bg-success", + Text: $"Task \"{t.Name}\" marked complete", + Who: who, + When: t.UpdatedAt + )); + } + + // Tasks created in the last 7 days (non-completed) + var newTasks = _taskData + .Where(t => !string.Equals(t.Status, "Completed", StringComparison.OrdinalIgnoreCase) + && t.CreatedAt >= cutoff) + .OrderByDescending(t => t.CreatedAt) + .Take(5); + + foreach (var t in newTasks) + { + items.Add(new ActivityItem( + Icon: "bi-clipboard-plus", + BadgeClass: "text-bg-primary", + Text: $"New task assigned: \"{t.Name}\"", + Who: t.User?.Name, + When: t.CreatedAt + )); + } + + // Recent purchase orders + var recentOrders = await Db.PurchaseOrders + .Where(po => po.LocationId == locationId && po.CreatedAt >= cutoff) + .OrderByDescending(po => po.CreatedAt) + .Take(5) + .ToListAsync(); + + foreach (var po in recentOrders) + { + var statusLabel = po.Status switch + { + PurchaseOrderStatus.Draft => "created (Draft)", + PurchaseOrderStatus.Submitted => "submitted", + PurchaseOrderStatus.PartiallyReceived => "partially received", + PurchaseOrderStatus.Received => "received", + PurchaseOrderStatus.Cancelled => "cancelled", + _ => po.Status.ToString() + }; + + items.Add(new ActivityItem( + Icon: "bi-cart", + BadgeClass: "text-bg-warning", + Text: $"Purchase order {po.OrderNumber} {statusLabel} — {po.VendorName}", + Who: null, + When: po.CreatedAt + )); + } + + _activityFeed = items + .OrderByDescending(i => i.When) + .Take(15) + .ToList(); + } + + private static string TimeAgo(DateTime utc) + { + var diff = DateTime.UtcNow - utc; + if (diff.TotalMinutes < 1) return "just now"; + if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago"; + if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago"; + return $"{(int)diff.TotalDays}d ago"; + } + private void NavigateToSignIn() { Nav.NavigateTo("/login", forceLoad: true); diff --git a/CulinaryCommandApp/Components/_Imports.razor b/CulinaryCommandApp/Components/_Imports.razor index d126159..ecacefb 100644 --- a/CulinaryCommandApp/Components/_Imports.razor +++ b/CulinaryCommandApp/Components/_Imports.razor @@ -13,4 +13,10 @@ @using CulinaryCommand.Components.Layout @using CulinaryCommand.Components.Custom @using CulinaryCommand.Components.Pages -@using BlazorBootstrap; \ No newline at end of file +@using BlazorBootstrap; +@using Plotly.Blazor +@using Plotly.Blazor.LayoutLib +@using Plotly.Blazor.Traces +@using Plotly.Blazor.Traces.ScatterLib +@using Plotly.Blazor.Traces.BarLib +@using Plotly.Blazor.Traces.PieLib \ No newline at end of file diff --git a/CulinaryCommandApp/CulinaryCommand.csproj b/CulinaryCommandApp/CulinaryCommand.csproj index 52dd607..9e5665e 100644 --- a/CulinaryCommandApp/CulinaryCommand.csproj +++ b/CulinaryCommandApp/CulinaryCommand.csproj @@ -29,6 +29,7 @@ all + PreserveNewest diff --git a/CulinaryCommandApp/Program.cs b/CulinaryCommandApp/Program.cs index 6343192..3d18971 100644 --- a/CulinaryCommandApp/Program.cs +++ b/CulinaryCommandApp/Program.cs @@ -29,13 +29,14 @@ var builder = WebApplication.CreateBuilder(args); +builder.Services.AddBlazorBootstrap(); + // // ===================== // UI // ===================== builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); -builder.Services.AddBlazorBootstrap(); // // ===================== // Cognito Authentication (MUST be before Build)