Skip to content
Closed
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
12 changes: 12 additions & 0 deletions Components/Account/Pages/Register.razor
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@

Logger.LogInformation("User created a new account with password.");

var int userCount = UserManager.User.Count();
var string? defaultRole = userCount <= 1 ? AppRoles.Admin : AppRoles.Driver;
var IdentityResult? roleResult = await USerManager.AddToRoleAsync(user, defalutRole)

if (!roleResult.Succeeded)
{
identityErrors = roleResult.Errors?
return;
}

Logger.LogInforamtion(message: "Assigned {Role} role to the new account.", defaultRole);

var userId = await UserManager.GetUserIdAsync(user);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
Expand Down
41 changes: 33 additions & 8 deletions Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,38 @@
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>

<AuthorizeView Policy="@AppPolicies.CanViewProducts">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="products">
<span class="bi bi-box-seam-fill-nav-menu" aria-hidden="true"></span> Products
</NavLink>
</div>
</Authorized>
</AuthorizeView>


<div class="nav-item px-3">
<NavLink class="nav-link" href="auth">
<span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Auth Required
</NavLink>
</div>
<AuthorizeView Policy="@@AppPolicies.CanEnterSales">
<Authorized>
<div class="nav-item px-3">
<navLink class="nav-link" href="sales">
<span class="bi bi-cash-coin" aria-hidden="true"></span> Sales Input
</navLink>
</div>
</Authorized>
</AuthorizeView>

<<<<<<< HEAD
<AuthorizeView Policy="@AppPolicies.CanManageDeliveryChecks">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="delivery-checks">
<span class="bi bi-truck" aria-hidden="true"></span> Delivery Checks
</NavLink>
</div>
</Authorized>
</AuthorizeView>
=======
<div class="nav-item px-3">
<NavLink class="nav-link" href="products">
<span class="bi bi-box-seam-fill-nav-menu" aria-hidden="true"></span> Catalog
Expand Down Expand Up @@ -56,6 +80,7 @@
<span class="bi bi-box-seam-fill-nav-menu" aria-hidden="true"></span> Outgoing/Incoming Boxes
</NavLink>
</div>
>>>>>>> main

<AuthorizeView>
<Authorized>
Expand Down Expand Up @@ -89,10 +114,10 @@
</AuthorizeView>

@* ── Admin-only navigation links ──
<AuthorizeView Roles="Admin"> checks the current user's roles.
<AuthorizeView Policy="@AppPolicies.CanManageUsers"> checks the current user's roles.
The inner content is only rendered if the user has the Admin role.
Non-admin users won't even see these links in the DOM. *@
<AuthorizeView Roles="Admin">
<AuthorizeView Policy="@AppPolicies.CanManageUsers">
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="admin/users">
Expand Down
2 changes: 1 addition & 1 deletion Components/Pages/Admin/StorageAdmin.razor
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@page "/admin/storage"
@using Microsoft.AspNetCore.Authorization
@using PuppetFestAPP.Web.Services
@attribute [Authorize(Roles = "Admin")]
@attribute [Authorize(Policy = AppPolicies.CanManageUsers)]
@rendermode InteractiveServer

@* ── Inject the services we need ── *@
Expand Down
8 changes: 4 additions & 4 deletions Components/Pages/Admin/UserManagement.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
and deleting users. Only users with the "Admin" role can access it.

KEY CONCEPTS:
• @attribute [Authorize(Roles = "Admin")] → Role-based access control.
• @attribute [Authorize(Policiy = AppPolicies.CanManageUsers)] → Role-based access control.
Non-admin users get an "access denied" response, not the page content.
• @rendermode InteractiveServer → Enables interactive features
(button clicks, form bindings) via a real-time SignalR connection.
Expand Down Expand Up @@ -38,7 +38,7 @@
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Identity
@using PuppetFestAPP.Web.Data
@attribute [Authorize(Roles = "Admin")]
@attribute [Authorize(Policy = AppPolicies.CanManageUsers)]
@rendermode InteractiveServer

@* ── Inject IServiceProvider instead of UserManager/RoleManager ──
Expand Down Expand Up @@ -187,7 +187,7 @@
private List<UserViewModel> users = new();
private string newUserEmail = "";
private string newUserPassword = "";
private string newUserRole = AppRoles.SalesStaff; // default selection for the "Role" dropdown in the "Create New User"
private string newUserRole = AppRoles.Driver; // default selection for the "Role" dropdown in the "Create New User"
private string statusMessage = "";
private string statusCssClass = "";

Expand Down Expand Up @@ -362,6 +362,6 @@
public string Email { get; set; } = "";
public List<string> Roles { get; set; } = new();
public bool EmailConfirmed { get; set; }
public string SelectedNewRole { get; set; } = AppRoles.SalesStaff;
public string SelectedNewRole { get; set; } = AppRoles.Driver;
}
}
195 changes: 195 additions & 0 deletions Components/Pages/DeliveryChecks.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
@page "/delivery-checks"
@using Microsoft.EntityFrameworkCore
@using PuppetFestAPP.Web.Data
@attribute [Authorize(Policy = AppPolicies.CanManageDeliveryChecks)]
@rendermode InteractiveServer
@inject IDbContextFactory<ApplicationDbContext> DbFactory

<PageTitle>Delivery Checks</PageTitle>

<h1>Delivery Checks</h1>

<p class="text-muted">
Confirm box and delivery checks for each product/location stock row.
</p>

@if (!string.IsNullOrWhiteSpace(statusMessage))
{
<div class="alert @statusCssClass alert-dismissible fade show" role="alert">
@statusMessage
<button type="button" class="btn-close" @onclick="() => statusMessage = string.Empty"></button>
</div>
}

@if (checkRows.Count == 0)
{
<div class="alert alert-info">No delivery check records are available.</div>
}
else
{
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-dark">
<tr>
<th>Product</th>
<th>Location</th>
<th class="text-end">Stock</th>
<th>Box Check</th>
<th>Delivery Check</th>
<th style="width: 240px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var row in checkRows)
{
<tr>
<td>@row.ProductName</td>
<td>@row.LocationName</td>
<td class="text-end">@row.Quantity</td>
<td>
@if (row.IsBoxChecked)
{
<span class="badge bg-success">Checked</span>
<span class="text-muted small ms-2">@FormatDate(row.BoxCheckedAt)</span>
}
else
{
<span class="badge bg-warning text-dark">Pending</span>
}
</td>
<td>
@if (row.IsDeliveryChecked)
{
<span class="badge bg-success">Checked</span>
<span class="text-muted small ms-2">@FormatDate(row.DeliveryCheckedAt)</span>
}
else
{
<span class="badge bg-warning text-dark">Pending</span>
}
</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-primary"
disabled="@row.IsBoxChecked"
@onclick="() => MarkBoxCheckedAsync(row)">
Box Checked
</button>
<button class="btn btn-sm btn-outline-primary"
disabled="@row.IsDeliveryChecked"
@onclick="() => MarkDeliveryCheckedAsync(row)">
Delivery Checked
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}

@code {
private List<DeliveryCheckRow> checkRows = new();
private string statusMessage = string.Empty;
private string statusCssClass = "alert-info";

protected override async Task OnInitializedAsync()
{
await LoadCheckRowsAsync();
}

private async Task LoadCheckRowsAsync()
{
using var context = await DbFactory.CreateDbContextAsync();

var productLocations = await context.ProductLocations
.Include(pl => pl.Product)
.Include(pl => pl.Location)
.OrderBy(pl => pl.Product.Name)
.ThenBy(pl => pl.Location.Name)
.ToListAsync();

checkRows = productLocations.Select(pl => new DeliveryCheckRow
{
ProductLocationId = pl.Id,
ProductName = pl.Product.Name,
LocationName = pl.Location.Name,
Quantity = pl.Quantity,
IsBoxChecked = pl.IsBoxChecked,
BoxCheckedAt = pl.BoxCheckedAt,
IsDeliveryChecked = pl.IsDeliveryChecked,
DeliveryCheckedAt = pl.DeliveryCheckedAt
}).ToList();
}

private async Task MarkBoxCheckedAsync(DeliveryCheckRow row)
{
await UpdateCheckAsync(
row,
updateBoxCheck: true,
successMessage: $"Box check recorded for {row.ProductName} at {row.LocationName}.");
}

private async Task MarkDeliveryCheckedAsync(DeliveryCheckRow row)
{
await UpdateCheckAsync(
row,
updateBoxCheck: false,
successMessage: $"Delivery check recorded for {row.ProductName} at {row.LocationName}.");
}

private async Task UpdateCheckAsync(
DeliveryCheckRow row,
bool updateBoxCheck,
string successMessage)
{
using var context = await DbFactory.CreateDbContextAsync();
var productLocation = await context.ProductLocations
.FirstOrDefaultAsync(pl => pl.Id == row.ProductLocationId);

if (productLocation is null)
{
statusMessage = "The selected stock record no longer exists.";
statusCssClass = "alert-danger";
await LoadCheckRowsAsync();
return;
}

var checkedAt = DateTime.UtcNow;

if (updateBoxCheck)
{
productLocation.IsBoxChecked = true;
productLocation.BoxCheckedAt = checkedAt;
}
else
{
productLocation.IsDeliveryChecked = true;
productLocation.DeliveryCheckedAt = checkedAt;
}

await context.SaveChangesAsync();

statusMessage = successMessage;
statusCssClass = "alert-success";
await LoadCheckRowsAsync();
}

private static string FormatDate(DateTime? value)
{
return value.HasValue ? value.Value.ToString("g") : string.Empty;
}

private sealed class DeliveryCheckRow
{
public int ProductLocationId { get; set; }
public string ProductName { get; set; } = string.Empty;
public string LocationName { get; set; } = string.Empty;
public int Quantity { get; set; }
public bool IsBoxChecked { get; set; }
public DateTime? BoxCheckedAt { get; set; }
public bool IsDeliveryChecked { get; set; }
public DateTime? DeliveryCheckedAt { get; set; }
}
}
3 changes: 2 additions & 1 deletion Components/Pages/ProductPages/Create.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@using Microsoft.EntityFrameworkCore
@using PuppetFestAPP.Web.Data
@using PuppetFestAPP.Web.Models
@attribute [Authorize(Policy = AppPolicies.CanEditProducts)]
@inject IDbContextFactory<ApplicationDbContext> DbFactory
@inject NavigationManager NavigationManager
@inject IWebHostEnvironment Environment
Expand Down Expand Up @@ -152,4 +153,4 @@
? $"/products/details/{newProduct.ParentProductId}"
: "/products");
}
}
}
1 change: 1 addition & 0 deletions Components/Pages/ProductPages/Delete.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@page "/products/delete/{Id:int}"
@using Microsoft.EntityFrameworkCore
@using PuppetFestAPP.Web.Data
@attribute [Authorize(Policy = AppPolicies.CanEditProducts)]
@rendermode InteractiveServer
@inject IDbContextFactory<PuppetFestAPP.Web.Data.ApplicationDbContext> DbFactory
@inject NavigationManager NavigationManager
Expand Down
Loading