diff --git a/CulinaryCommandApp/Components/App.razor b/CulinaryCommandApp/Components/App.razor index 46735c8..d2c92c8 100644 --- a/CulinaryCommandApp/Components/App.razor +++ b/CulinaryCommandApp/Components/App.razor @@ -29,8 +29,6 @@ - - diff --git a/CulinaryCommandApp/Components/Pages/Dashboard.razor b/CulinaryCommandApp/Components/Pages/Dashboard.razor index f3cd977..b251589 100644 --- a/CulinaryCommandApp/Components/Pages/Dashboard.razor +++ b/CulinaryCommandApp/Components/Pages/Dashboard.razor @@ -1,6 +1,5 @@ @page "/dashboard" @rendermode InteractiveServer -@implements IDisposable @using CulinaryCommand.Components.Custom @using CulinaryCommand.Services.UserContextSpace @@ -8,19 +7,12 @@ @using CulinaryCommandApp.AIDashboard.Services.DTOs @using CulinaryCommand.Services @using CulinaryCommandApp.Inventory.DTOs -@using CulinaryCommandApp.Inventory.Services.Interfaces -@using CulinaryCommand.Data.Entities -@using CulinaryCommand.PurchaseOrder.Entities -@using Microsoft.EntityFrameworkCore - +@using CulinaryCommandApp.Data.Models @inject Microsoft.AspNetCore.Hosting.IWebHostEnvironment Env @inject NavigationManager Nav @inject LocationState LocationState @inject AIReportingService ReportingService @inject IUserContextService UserCtx -@inject IInventoryManagementService InventorySvc -@inject ITaskAssignmentService TaskSvc -@inject AppDbContext Db @if (!_ready) { @@ -29,15 +21,13 @@ else if (!_isSignedIn) {
- You're not signed in. + You’re not signed in.
} else {
- -
Kitchen Operations
@@ -53,7 +43,7 @@ else
Low Stock
-
@_lowStockItems.Count
+
@_lowStockItems.Count
Open Tasks
@@ -78,269 +68,93 @@ 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 items sufficiently stocked. -

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

No tasks for this location.

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

No completed tasks yet.

- } -
-
-
- - -
-
-
- Live Activity (Last 7 Days) - - @_activityRefreshedAt.ToLocalTime().ToString("HH:mm:ss") - -
-
- @if (_activityChartData.Any()) - { - - } - else - { -

No recent activity in the last 7 days.

- } -
-
-
-
- -
- - -
-
- Weekly Report Analysis - @if (_aiAnalysisObj != null) - { - Generated: @_aiAnalysisObj.GeneratedAt?.ToLocalTime().ToString("g") - } -
+
+
Weekly Report Analysis
@if (_aiLoading) { -
-
-

Loading analysis from Gemini…

-
+
Loading analysis from Gemini...
} else if (_aiAnalysisObj == null) { -

No analysis available.

+
No analysis available.
} else { -
- -
-
+
+
@_aiAnalysisObj.Title

@_aiAnalysisObj.Summary

+

Generated: @_aiAnalysisObj.GeneratedAt?.ToLocalTime().ToString("g")

+

Confidence: @((_aiAnalysisObj.Confidence ?? 0).ToString("P0"))

-
-
-
AI Confidence
-
- -
-
-
- - @if (_aiMetricsData.Any()) - { -
-
-
Key Metrics
-
- -
-
+
+
+ @if (_aiAnalysisObj.Metrics != null && _aiAnalysisObj.Metrics.Any()) + { + foreach (var m in _aiAnalysisObj.Metrics) + { +
+
+
+
@m.Label
+

@m.Value @(!string.IsNullOrWhiteSpace(m.Unit) ? m.Unit : "")

+
+
+
+ } + }
- } -
- - -
- @if (_aiAnalysisObj.Recommendations != null && _aiAnalysisObj.Recommendations.Any()) - { -
-
-
Recommendations
+
+ @if (_aiAnalysisObj.Recommendations != null && _aiAnalysisObj.Recommendations.Any()) + { +
Recommendations
    @foreach (var r in _aiAnalysisObj.Recommendations) { -
  • - @r -
  • +
  • @r
  • }
-
+ }
- } +
+
- @if (_aiAnalysisObj.Sections != null && _aiAnalysisObj.Sections.Any()) - { -
-
- @foreach (var s in _aiAnalysisObj.Sections) - { -
-
-
-
@s.Heading
-

@s.Body

-
-
+ @if (_aiAnalysisObj.Sections != null && _aiAnalysisObj.Sections.Any()) + { +
+ @foreach (var s in _aiAnalysisObj.Sections) + { +
+
+
+
@s.Heading
+

@s.Body

- } +
-
- } -
+ } +
+ } - @if (_aiAnalysisObj.Anomalies != null && _aiAnalysisObj.Anomalies.Any()) {
-
Anomalies
+
Anomalies
- - - - +
RowReason
+ @foreach (var a in _aiAnalysisObj.Anomalies) { - - - + }
RowReason
@a.Row@a.Reason
@a.Row@a.Reason
@@ -350,83 +164,27 @@ else }
-
} @code { - - // ── Auth / location ─────────────────────────────────────────────── - private bool _ready; - private bool _isSignedIn; + private bool _ready; + private bool _isSignedIn; private string? _role; - private int? _locationId; - // ── Raw data ────────────────────────────────────────────────────── private List _lowStockItems = new(); - private List _taskData = new(); - private List _recentOrders = new(); + private List _taskData = new(); - // ── Plotly – shared config (no mode bar) ────────────────────────── - private Config _dashChartConfig = new() - { - Responsive = true, - DisplayModeBar = Plotly.Blazor.ConfigLib.DisplayModeBarEnum.False - }; - - // ── Card 2: Task Status Donut ───────────────────────────────────── - private IList _taskStatusChartData = new List(); - private Layout _taskStatusLayout = new(); - - // ── Card 3: Employee Task Completion (table) ────────────────────── - private record EmployeeCount(string Name, int Count); - private List _empTaskCounts = new(); - - // ── Card 4: Live Activity Timeline ─────────────────────────────── - private IList _activityChartData = new List(); - private Layout _activityLayout = new(); - private DateTime _activityRefreshedAt = DateTime.UtcNow; - private System.Threading.Timer? _refreshTimer; - - // ── AI analysis ─────────────────────────────────────────────────── - private bool _aiLoading; + private bool _aiLoading; private string? _aiAnalysis; private AIAnalysisResultDTO? _aiAnalysisObj; - private bool _aiLoadedOnce; - - private Config _aiChartConfig = new() - { - Responsive = true, - DisplayModeBar = Plotly.Blazor.ConfigLib.DisplayModeBarEnum.False - }; - private IList _aiGaugeData = new List(); - private Layout _aiGaugeLayout = new(); - private IList _aiMetricsData = new List(); - private Layout _aiMetricsLayout = new(); - - // ───────────────────────────────────────────────────────────────── + private bool _aiLoadedOnce; 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 LoadDashboardDataAsync(_locationId.Value); - - // Auto-refresh the activity chart every 30 s - _refreshTimer = new System.Threading.Timer(async _ => - { - await LoadActivityAsync(_locationId!.Value); - BuildActivityChart(); - await InvokeAsync(StateHasChanged); - }, null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); - } + _role = ctx.User?.Role; _ready = true; } @@ -438,298 +196,42 @@ else if (_isSignedIn && !_aiLoadedOnce) { _aiLoadedOnce = true; - _aiLoading = true; + _aiLoading = true; StateHasChanged(); - try - { - var csvPath = Path.Combine(Env.ContentRootPath, "AIDashboard", "Services", "Reporting", "test_data.csv"); - _aiAnalysis = await ReportingService.AnalyzeCsvAsync(csvPath); - - if (!string.IsNullOrWhiteSpace(_aiAnalysis)) + try{ + if (!string.IsNullOrWhiteSpace(_aiAnalysis)) { - var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - _aiAnalysisObj = System.Text.Json.JsonSerializer.Deserialize(_aiAnalysis, options); - - if (_aiAnalysisObj != null) - BuildAICharts(_aiAnalysisObj); - } + 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; + _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 LoadDashboardDataAsync(int locationId) - { - var allInventory = await InventorySvc.GetItemsByLocationAsync(locationId); - _lowStockItems = allInventory - .Where(i => i.IsLowStock) - .OrderBy(i => i.CurrentQuantity) - .ToList(); - - _taskData = await TaskSvc.GetByLocationAsync(locationId); - - await LoadActivityAsync(locationId); - BuildOperationalCharts(); - } - - private async Task LoadActivityAsync(int locationId) - { - var cutoff = DateTime.UtcNow.AddDays(-7); - _recentOrders = await Db.PurchaseOrders - .Where(po => po.LocationId == locationId && po.CreatedAt >= cutoff) - .OrderByDescending(po => po.CreatedAt) - .Take(25) - .ToListAsync(); - - _activityRefreshedAt = DateTime.UtcNow; - } - - // ── Chart builders ──────────────────────────────────────────────── - - private void BuildOperationalCharts() - { - BuildTaskStatusChart(); - BuildEmpTaskCounts(); - BuildActivityChart(); - } - - private void BuildEmpTaskCounts() - { - _empTaskCounts = _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 void BuildTaskStatusChart() - { - if (!_taskData.Any()) return; - - var groups = _taskData - .GroupBy(t => string.IsNullOrWhiteSpace(t.Status) ? "Unknown" : t.Status) - .ToDictionary(g => g.Key, g => g.Count()); - - _taskStatusChartData = new List - { - new Pie - { - Labels = groups.Keys.Select(k => (object)k).ToList(), - Values = groups.Values.Select(v => (object)v).ToList(), - Hole = (decimal?)0.45, - Marker = new Plotly.Blazor.Traces.PieLib.Marker - { - Colors = groups.Keys.Select(k => (object)StatusColor(k)).ToList() - } - } - }; - - _taskStatusLayout = new Layout - { - PaperBgColor = "transparent", - PlotBgColor = "transparent", - Height = 280, - Margin = new Margin { T = 20, B = 20, L = 10, R = 10 }, - ShowLegend = true, - Legend = new List - { - new() { Orientation = Plotly.Blazor.LayoutLib.LegendLib.OrientationEnum.H, Y = (decimal?)-0.1 } - } - }; - } - - private void BuildActivityChart() - { - var cutoff = DateTime.UtcNow.AddDays(-7); - var traces = new List(); - - // Completed tasks - var completed = _taskData - .Where(t => string.Equals(t.Status, "Completed", StringComparison.OrdinalIgnoreCase) - && t.UpdatedAt >= cutoff) - .OrderBy(t => t.UpdatedAt) - .Take(30) - .ToList(); - - if (completed.Any()) - { - traces.Add(new Scatter - { - Name = "Task Completed", - Mode = Plotly.Blazor.Traces.ScatterLib.ModeFlag.Markers, - X = completed.Select(t => (object?)t.UpdatedAt.ToString("yyyy-MM-dd HH:mm:ss")).ToList(), - Y = completed.Select(_ => (object?)"Task Completed").ToList(), - CustomData = completed.Select(t => (object?)(t.Name ?? "—")).ToList(), - HoverTemplate = "%{customdata}
%{x}", - Marker = new Plotly.Blazor.Traces.ScatterLib.Marker { Color = "#198754", Size = (decimal?)14 } - }); - } - - var created = _taskData - .Where(t => !string.Equals(t.Status, "Completed", StringComparison.OrdinalIgnoreCase) - && t.CreatedAt >= cutoff) - .OrderBy(t => t.CreatedAt) - .Take(20) - .ToList(); - - if (created.Any()) - { - traces.Add(new Scatter - { - Name = "Task Created", - Mode = Plotly.Blazor.Traces.ScatterLib.ModeFlag.Markers, - X = created.Select(t => (object?)t.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")).ToList(), - Y = created.Select(_ => (object?)"Task Created").ToList(), - CustomData = created.Select(t => (object?)(t.Name ?? "—")).ToList(), - HoverTemplate = "%{customdata}
%{x}", - Marker = new Plotly.Blazor.Traces.ScatterLib.Marker { Color = "#0d6efd", Size = (decimal?)14 } - }); - } - - if (_recentOrders.Any()) - { - traces.Add(new Scatter - { - Name = "Purchase Order", - Mode = Plotly.Blazor.Traces.ScatterLib.ModeFlag.Markers, - X = _recentOrders.Select(po => (object?)po.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")).ToList(), - Y = _recentOrders.Select(_ => (object?)"Purchase Order").ToList(), - CustomData = _recentOrders.Select(po => (object?)$"{po.OrderNumber} – {po.VendorName}").ToList(), - HoverTemplate = "%{customdata}
%{x}", - Marker = new Plotly.Blazor.Traces.ScatterLib.Marker { Color = "#fd7e14", Size = (decimal?)14 } - }); - } - - _activityChartData = traces; - - _activityLayout = new Layout - { - PaperBgColor = "transparent", - PlotBgColor = "transparent", - Height = 280, - Margin = new Margin { T = 10, B = 40, L = 10, R = 10 }, - ShowLegend = true, - Legend = new List - { - new() { Orientation = Plotly.Blazor.LayoutLib.LegendLib.OrientationEnum.H, Y = (decimal?)-0.2 } - }, - XAxis = new List - { - new() { Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date } - }, - YAxis = new List - { - new() { AutoMargin = Plotly.Blazor.LayoutLib.YAxisLib.AutoMarginFlag.True } - } - }; - } - - // ── AI chart builders ───────────────────────────────────────────── - - private void BuildAICharts(AIAnalysisResultDTO result) + private void NavigateToSignIn() { - var confidencePercent = (decimal?)((result.Confidence ?? 0) * 100); - - _aiGaugeData = new List - { - new Indicator - { - Mode = Plotly.Blazor.Traces.IndicatorLib.ModeFlag.Gauge - | Plotly.Blazor.Traces.IndicatorLib.ModeFlag.Number, - Value = confidencePercent, - Number = new Plotly.Blazor.Traces.IndicatorLib.Number { Suffix = "%" }, - Gauge = new Plotly.Blazor.Traces.IndicatorLib.Gauge - { - Axis = new Plotly.Blazor.Traces.IndicatorLib.GaugeLib.Axis { Range = new List { 0, 100 } }, - Bar = new Plotly.Blazor.Traces.IndicatorLib.GaugeLib.Bar { Color = "#198754" }, - Steps = new List - { - new() { Range = new List { 0, 50 }, Color = "#f8d7da" }, - new() { Range = new List { 50, 75 }, Color = "#fff3cd" }, - new() { Range = new List { 75, 100 }, Color = "#d1e7dd" } - } - } - } - }; - - _aiGaugeLayout = new Layout - { - PaperBgColor = "transparent", - PlotBgColor = "transparent", - Height = 220, - Margin = new Margin { T = 10, B = 10, L = 20, R = 20 } - }; - - if (result.Metrics == null || !result.Metrics.Any()) return; - - var parsed = result.Metrics - .Select(m => (label: m.Label ?? "", num: TryParseMetric(m.Value))) - .Where(x => x.num.HasValue) - .ToList(); - - if (!parsed.Any()) return; - - _aiMetricsData = new List - { - new Bar - { - Y = parsed.Select(x => (object?)x.label).ToList(), - X = parsed.Select(x => (object?)x.num).ToList(), - Orientation = Plotly.Blazor.Traces.BarLib.OrientationEnum.H, - Marker = new Plotly.Blazor.Traces.BarLib.Marker - { - Color = new List { "#0d6efd" } - } - } - }; - - _aiMetricsLayout = new Layout - { - PaperBgColor = "transparent", - PlotBgColor = "transparent", - Height = 220, - Margin = new Margin { T = 10, B = 30, L = 10, R = 10 }, - XAxis = new List { new() { AutoMargin = Plotly.Blazor.LayoutLib.XAxisLib.AutoMarginFlag.True } }, - YAxis = new List { new() { AutoMargin = Plotly.Blazor.LayoutLib.YAxisLib.AutoMarginFlag.True } } - }; + Nav.NavigateTo("/login", forceLoad: true); } - - // ── Helpers ─────────────────────────────────────────────────────── - - private static string StatusColor(string status) => status.ToLowerInvariant() switch - { - "completed" => "#198754", - "pending" => "#ffc107", - "in progress" => "#0d6efd", - "in-progress" => "#0d6efd", - "cancelled" => "#dc3545", - _ => "#6c757d" - }; - - private static double? TryParseMetric(string? raw) - { - if (string.IsNullOrWhiteSpace(raw)) return null; - var cleaned = System.Text.RegularExpressions.Regex.Replace(raw, @"[^0-9.\-]", ""); - return double.TryParse(cleaned, System.Globalization.NumberStyles.Any, - System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : null; - } - - private void NavigateToSignIn() => Nav.NavigateTo("/login", forceLoad: true); - - public void Dispose() => _refreshTimer?.Dispose(); } diff --git a/CulinaryCommandApp/Components/_Imports.razor b/CulinaryCommandApp/Components/_Imports.razor index bb5118c..8306129 100644 --- a/CulinaryCommandApp/Components/_Imports.razor +++ b/CulinaryCommandApp/Components/_Imports.razor @@ -12,7 +12,4 @@ @using CulinaryCommand.Services @using CulinaryCommand.Components.Layout @using CulinaryCommand.Components.Custom -@using CulinaryCommand.Components.Pages -@using Plotly.Blazor -@using Plotly.Blazor.Traces -@using Plotly.Blazor.LayoutLib \ No newline at end of file +@using CulinaryCommand.Components.Pages \ No newline at end of file diff --git a/CulinaryCommandApp/CulinaryCommand.csproj b/CulinaryCommandApp/CulinaryCommand.csproj index 3929f7a..1b776be 100644 --- a/CulinaryCommandApp/CulinaryCommand.csproj +++ b/CulinaryCommandApp/CulinaryCommand.csproj @@ -28,7 +28,6 @@ all - PreserveNewest diff --git a/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor b/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor index e6a1481..89ebcf7 100644 --- a/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor +++ b/CulinaryCommandApp/Inventory/Pages/Inventory/InventoryCatalog.razor @@ -34,28 +34,10 @@ } else { -
-
-
Kitchen Operations
-

Inventory Catalog

-

Manage your location's ingredient catalog.

-
-
-
-
Location
-
@(LocationState.CurrentLocation?.Name ?? "—")
-
-
-
-
-
Total Items
-
@itemCatalog.Count
-
-
-
Low Stock
-
@itemCatalog.Count(i => i.IsLowStock)
-
-
+
+
+

Inventory Catalog

+

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

diff --git a/CulinaryCommandApp/Program.cs b/CulinaryCommandApp/Program.cs index c54972c..2c9c932 100644 --- a/CulinaryCommandApp/Program.cs +++ b/CulinaryCommandApp/Program.cs @@ -119,14 +119,7 @@ // ===================== // AI Services // ===================== -builder.Services.AddSingleton(sp => -{ - var config = sp.GetRequiredService(); - var apiKey = config["Google:ApiKey"] - ?? throw new InvalidOperationException( - "Missing configuration key 'Google:ApiKey'. Add it to appsettings or set the GOOGLE_API_KEY environment variable."); - return new Client(apiKey: apiKey); -}); +builder.Services.AddSingleton(_ => new Client()); builder.Services.AddScoped(); //