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
13 changes: 13 additions & 0 deletions CulinaryCommandApp/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="css/app.css" />
Expand All @@ -31,6 +34,16 @@
crossorigin="anonymous"></script>

<script src="_framework/blazor.server.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js" integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q" crossorigin="anonymous"></script>
<!-- Add chart.js reference if chart components are used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.0.1/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Add sortable.js reference if SortableList component is used in your application. -->
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<script src="_content/Plotly.Blazor/plotly-blazor.js"></script>
</body>

</html>
338 changes: 326 additions & 12 deletions CulinaryCommandApp/Components/Pages/Dashboard.razor

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions CulinaryCommandApp/Components/Pages/UserSettings/Settings.razor
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
<i class="fas fa-building me-2"></i>Company
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link @(ActiveTab == "inventory-config" ? "active" : "")"
@onclick="@(() => NavigateToTab("inventory-config"))" type="button">
Inventory Config
</button>
</li>
</ul>

<!-- Tab Content -->
Expand Down Expand Up @@ -87,6 +93,10 @@
<SettingsCompany />
break;

case "inventory-config":
<SettingsInventoryConfigurations />
break;

default:
<SettingsProfile />
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
@rendermode InteractiveServer

@using CulinaryCommand.Services.UserContextSpace
@using CulinaryCommandApp.Inventory.Entities
@using Unit = CulinaryCommandApp.Inventory.Entities.Unit
@using CulinaryCommandApp.Inventory.Services.Interfaces
@using System.ComponentModel.DataAnnotations

@inject IUnitService UnitService
@inject IUserContextService UserCtx
@inject NavigationManager Nav

<div class="tab-pane active">
<h4 class="mb-4">
Inventory Configurations
</h4>

@if (!_ready)
{
<div class="d-flex justify-content-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
}
else
{
<!-- Unit of Measurement Form -->
<div class="security-section mb-4">
<h5 class="mb-1">
Unit of Measurement
</h5>
<p class="text-muted small mb-3">Define standard units for ingredients and recipes in your kitchen</p>

<EditForm Model="@_form" OnValidSubmit="HandleSave">
<DataAnnotationsValidator />

<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Unit Name <span class="text-danger">*</span></label>
<InputText class="form-control" @bind-Value="_form.Name" placeholder="e.g. Pounds, Ounces, Piece" />
<div class="form-text">Full name of the measurement unit</div>
<ValidationMessage For="() => _form.Name" />
</div>
<div class="col-md-6">
<label class="form-label">Abbreviation <span class="text-danger">*</span></label>
<InputText class="form-control" @bind-Value="_form.Abbreviation" placeholder="e.g. lbs, oz, pc" />
<div class="form-text">Short form for display</div>
<ValidationMessage For="() => _form.Abbreviation" />
</div>
@* <div class="col-md-6">
<label class="form-label">Conversion Factor</label>
<InputNumber class="form-control" @bind-Value="_form.ConversionFactor" />
<div class="form-text">Multiplier relative to base unit (e.g. 1000 for kg if gram is base)</div>
</div> *@
</div>

@if (!string.IsNullOrEmpty(_saveError))
{
<div class="alert alert-danger mt-3 py-2">
<i class="bi bi-exclamation-circle me-2"></i>@_saveError
</div>
}

<div class="d-flex justify-content-end gap-2 mt-3">
@if (_editingId.HasValue)
{
<button type="button" class="btn btn-secondary" @onclick="CancelEdit">Cancel</button>
<button type="submit" class="btn btn-success" disabled="@_saving">
@(_saving ? "Saving..." : "Update")
</button>
}
else
{
<button type="button" class="btn btn-secondary" @onclick="ResetForm">Cancel</button>
<button type="submit" class="btn btn-success" disabled="@_saving">
@(_saving ? "Saving..." : "Save")
</button>
}
</div>
</EditForm>
</div>

<!-- Existing Units Table -->
<div class="security-section">
<h5 class="mb-1">
<i class="bi bi-list-ul me-2"></i>Existing Units
</h5>
<p class="text-muted small mb-3">Units that have been set and are usable across ingredients and recipes</p>

@if (!_units.Any())
{
<div class="text-center text-muted py-3">
<i class="bi bi-rulers fs-2 d-block mb-2"></i>
No units configured yet. Add one above to get started.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Unit Name</th>
<th>Abbreviation</th>
<th>Conv. Factor</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var unit in _units)
{
<tr class="@(_editingId == unit.Id ? "table-warning" : "")">
<td><strong>@unit.Name</strong></td>
<td><span class="badge bg-secondary">@unit.Abbreviation</span></td>
<td class="text-muted">@unit.ConversionFactor</td>
<td class="text-end">
<button class="btn btn-outline-secondary btn-sm me-1"
title="Edit unit"
@onclick="() => StartEdit(unit)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-outline-danger btn-sm"
title="Delete unit"
@onclick="() => ConfirmDelete(unit)">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="text-end mt-2">
<small class="text-muted">@_units.Count unit(s) configured</small>
</div>
}
</div>
}
</div>

<!-- Delete Confirmation Modal -->
@if (_unitToDelete is not null)
{
<div class="modal fade show d-block" tabindex="-1" style="background:rgba(0,0,0,0.4);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Unit</h5>
<button type="button" class="btn-close" @onclick="CancelDelete"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete <strong>@_unitToDelete.Name (@_unitToDelete.Abbreviation)</strong>?</p>
<p class="text-danger small mb-0">
<i class="bi bi-exclamation-triangle me-1"></i>
This may affect ingredients and inventory transactions that reference this unit.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelDelete">Cancel</button>
<button type="button" class="btn btn-danger" disabled="@_deleting" @onclick="DeleteUnit">
@if (_deleting)
{
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
}
Delete
</button>
</div>
</div>
</div>
</div>
}

@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<Unit> _units = new();

private UnitFormModel _form = new();

protected override async Task OnInitializedAsync()
{
_ctx = await UserCtx.GetAsync();

if (_ctx.IsAuthenticated != true)
{
Nav.NavigateTo("/login", true);
return;
}

if (_ctx.User is not null)
{
_allowed = true;
_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;
}
}
9 changes: 8 additions & 1 deletion CulinaryCommandApp/Components/_Imports.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,11 @@
@using CulinaryCommand.Services
@using CulinaryCommand.Components.Layout
@using CulinaryCommand.Components.Custom
@using CulinaryCommand.Components.Pages
@using CulinaryCommand.Components.Pages
@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
Loading
Loading