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
30 changes: 5 additions & 25 deletions CulinaryCommandApp/Components/App.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@namespace CulinaryCommand.Components
@inject NavigationManager Nav
@inject CulinaryCommand.Services.AuthService Auth
@using Microsoft.AspNetCore.Components.Authorization

<!DOCTYPE html>
<html lang="en">
Expand All @@ -12,6 +11,7 @@
<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" />
<link rel="stylesheet" href="css/landing.css" />
<link rel="stylesheet" href="CulinaryCommand.styles.css" />
<link rel="icon" type="image/png" href="images/favicon.png" />
<HeadOutlet />
Expand All @@ -21,9 +21,9 @@
<body>
@* <Routes @rendermode=RenderMode.InteractiveServer /> *@

<CascadingValue Value="Auth">
<Routes />
</CascadingValue>
<CascadingAuthenticationState>
<Routes @rendermode="InteractiveServer"/>
</CascadingAuthenticationState>


<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
Expand All @@ -34,23 +34,3 @@
</body>

</html>

@code {
@* private bool _initialized;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !_initialized)
{
// Don't re-hydrate if user is already on /signin or /signup
var path = Nav.Uri.ToLower();
if (!path.Contains("/signin") && !path.Contains("/signup"))
{
await Auth.InitializeFromJsAsync();
}

_initialized = true;
StateHasChanged();
}
} *@
}
123 changes: 60 additions & 63 deletions CulinaryCommandApp/Components/Custom/LocationSelector.razor
Original file line number Diff line number Diff line change
@@ -1,50 +1,43 @@
@using CulinaryCommand.Models
@using CulinaryCommand.Services
@using CulinaryCommand.Data.Entities
@using System.Xml
@using System.Text.Json

@inject AuthService Auth
@using CulinaryCommand.Services.UserContextSpace

@inject ILocationService LocationService
@inject LocationState LocationState
@inject IJSRuntime JS


@inject IUserContextService UserCtx
@inject IJSRuntime JS
@inject NavigationManager NavigationManager

<!-- Location Dropdown -->
<div class="dropdown me-3">
<button class="btn btn-light dropdown-toggle d-flex align-items-center py-2 pe-3" type="button" id="businessMenu"
data-bs-toggle="dropdown" aria-expanded="false" style="height:3rem;" title="View and edit restaurants">
data-bs-toggle="dropdown" aria-expanded="false" style="height:3rem;" title="View and edit restaurants">

<div class="icon-box d-flex align-items-center justify-content-center me-2"
style="background-color:#d3ddd5; width:2.5rem; height:2.5rem; border-radius:0.45rem;">
style="background-color:#d3ddd5; width:2.5rem; height:2.5rem; border-radius:0.45rem;">
<i class="bi bi-building fs-4" style="color:#688f72;"></i>
</div>

<span>@(LocationState.CurrentLocation?.Name ?? "Select Location")</span>
</button>

<ul class="dropdown-menu dropdown-menu-end shadow-lg" aria-labelledby="businessMenu">
@if (ManagedLocations.Any())
{
<li>
<h6 class="dropdown-header">Locations</h6>
</li>

<li><h6 class="dropdown-header">Locations</h6></li>

@foreach (Location location in ManagedLocations ?? Enumerable.Empty<Location>())
@foreach (var location in ManagedLocations)
{
<li>
<div
class="dropdown-item d-flex justify-content-between align-items-center @(LocationState.CurrentLocation == location ? "active" : "")">
<div class="dropdown-item d-flex justify-content-between align-items-center @(LocationState.CurrentLocation?.Id == location.Id ? "active" : "")">
<span class="flex-grow-1" style="cursor: pointer;"
@onclick="() => SetLocation(location)">
@onclick="() => SetLocation(location)">
@location.Name
</span>

<button class="btn btn-link p-0 ms-2 edit-btn" title="Edit @location.Name"
@onclick="() => EditLocation(location)" @onclick:stopPropagation="true">
@onclick="() => EditLocation(location)" @onclick:stopPropagation="true">
<i class="bi bi-pencil-fill fs-9 text-muted"></i>
</button>
</div>
Expand All @@ -55,80 +48,84 @@
{
<li>
<div class="d-flex justify-content-between align-items-center"
style="padding: 0.5rem 1rem; font-size: 0.95rem;">
<span class="flex-grow-1" style="cursor: pointer;">
No locations.
</span>
@* <button class="btn btn-link p-0 ms-2 edit-btn"
@onclick="() => AddRestaurant()" @onclick:stopPropagation="true">
<i class="bi bi-plus-square-fill fs-9 text-active"></i>
</button> *@
style="padding: 0.5rem 1rem; font-size: 0.95rem;">
<span class="flex-grow-1">No locations.</span>
</div>
</li>
}

</ul>
</div>

@code {
private List<Location> ManagedLocations { get; set; } = new();
private bool hydrated = false;
private bool _loadedOnce;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Wait for auth hydration
await Auth.EnsureHydratedAsync();
if (!firstRender || _loadedOnce) return;
_loadedOnce = true;

if (!Auth.IsSignedIn)
{
// Do NOT redirect here — navbar dropdown is visible on all pages,
// so returning silently is correct
return;
}

// 1. Load cached instantly
ManagedLocations = await LoadCachedLocationsAsync();
// 1) Load cached instantly (never fails hard)
ManagedLocations = await LoadCachedLocationsAsync();
if (LocationState.CurrentLocation == null && ManagedLocations.Any())
await LocationState.SetCurrentLocationAsync(ManagedLocations.First());

// 2. Load fresh server data
await LoadLocationsFromServerAsync();
StateHasChanged();

hydrated = true;
// 2) Load fresh server data if we can resolve user context
await LoadLocationsFromServerAsync();

StateHasChanged();
}
StateHasChanged();
}


private async Task<List<Location>> LoadCachedLocationsAsync()
{
var json = await JS.InvokeAsync<string>("localStorage.getItem", "cc_locations");
return string.IsNullOrWhiteSpace(json)
? new List<Location>()
: (JsonSerializer.Deserialize<List<Location>>(json) ?? new List<Location>());
try
{
var json = await JS.InvokeAsync<string?>("localStorage.getItem", "cc_locations");
return string.IsNullOrWhiteSpace(json)
? new List<Location>()
: (JsonSerializer.Deserialize<List<Location>>(json) ?? new List<Location>());
}
catch
{
return new List<Location>();
}
}

private async Task LoadLocationsFromServerAsync()
{
if (Auth.UserRole == "Admin")
var ctx = await UserCtx.GetAsync();

// If not authenticated / not linked to DB user yet, do nothing.
if (ctx.User?.Id is null) return;

List<Location> fresh;

if (ctx.User?.Role == "Admin")
{
ManagedLocations = await LocationService.GetLocationsByCompanyAsync(Auth.CompanyId!.Value);
if (ctx.User.CompanyId is null) return;
fresh = await LocationService.GetLocationsByCompanyAsync(ctx.User?.CompanyId.Value);
}
else if (Auth.UserRole == "Manager")
else if (ctx.User?.Role == "Manager")
{
ManagedLocations = await LocationService.GetLocationsByManagerAsync(Auth.UserId!.Value);
fresh = await LocationService.GetLocationsByManagerAsync(ctx.User?.Id);
}
else {
ManagedLocations = await LocationService.GetLocationsByEmployeeAsync(Auth.UserId!.Value);
else
{
fresh = await LocationService.GetLocationsByEmployeeAsync(ctx.User?.Id);
}

// Update localStorage
await JS.InvokeAsync<object>("localStorage.setItem",
"cc_locations",
JsonSerializer.Serialize(ManagedLocations));
ManagedLocations = fresh ?? new List<Location>();

// Cache
try
{
await JS.InvokeAsync<object>("localStorage.setItem", "cc_locations", JsonSerializer.Serialize(ManagedLocations));
}
catch { /* ignore caching failures */ }

// Set a default current location if none
// Default current location
if (LocationState.CurrentLocation == null && ManagedLocations.Any())
await LocationState.SetCurrentLocationAsync(ManagedLocations.First());
}
Expand All @@ -142,4 +139,4 @@
{
NavigationManager.NavigateTo($"/settings/locations/{location.Id}");
}
}
}
2 changes: 0 additions & 2 deletions CulinaryCommandApp/Components/Custom/LogoutWidget.razor
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
@rendermode InteractiveServer
@inject CulinaryCommand.Services.AuthService Auth
@inject NavigationManager Nav

<button class="btn btn-outline-danger" @onclick="Logout">Logout</button>

@code {
private async Task Logout()
{
await Auth.LogoutAsync();
Nav.NavigateTo("/signin", true); // hard reload
}
}
58 changes: 26 additions & 32 deletions CulinaryCommandApp/Components/Custom/ProfileDropdown.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@using CulinaryCommand.Services
@inject AuthService Auth
@inject IJSRuntime JS
@using CulinaryCommand.Services.UserContextSpace
@inject IUserContextService UserCtx
@inject NavigationManager Nav
@rendermode InteractiveServer

Expand All @@ -18,18 +18,18 @@
style="width:2.5rem; height:2.5rem;" />

<div class="text-start">
<div class="fw-semibold">@Auth.UserEmail</div>
<small class="text-muted">@Auth.UserRole</small>
<div class="fw-semibold">@UserEmail</div>
<small class="text-muted">@UserRole</small>
</div>
</button>

<ul class="dropdown-menu dropdown-menu-end shadow-lg" aria-labelledby="profileMenu">
<li><h6 class="dropdown-header">@Auth.Company</h6></li>
<li><h6 class="dropdown-header">@CompanyName</h6></li>
<li><a class="dropdown-item" href="/profile">View Profile</a></li>
<li><a class="dropdown-item" href="/settings">Settings</a></li>
<li><hr class="dropdown-divider" /></li>
<li>
<button class="dropdown-item text-danger fw-semibold" @onclick="() => Logout()">
<button class="dropdown-item text-danger fw-semibold" @onclick="Logout">
<i class="bi bi-box-arrow-right me-2"></i> Logout
</button>
</li>
Expand All @@ -39,40 +39,34 @@
@code {
private string UserEmail = "Guest";
private string UserRole = "";
private string Company = "";
private bool _hydrated = false;
private string CompanyName = "";

protected override async Task OnAfterRenderAsync(bool firstRender)
protected override async Task OnInitializedAsync()
{
if (firstRender) {
var ctx = await UserCtx.GetAsync();
UserEmail = string.IsNullOrWhiteSpace(ctx.Email) ? "Guest" : ctx.Email;
UserRole = ctx.User?.Role ?? "";
CompanyName = ctx.User?.Company?.Name ?? "";
}

private bool _loaded;

if (!_hydrated)
{
// Load from localStorage
UserEmail = await JS.InvokeAsync<string>("localStorage.getItem", "cc_email") ?? "Guest";
UserRole = await JS.InvokeAsync<string>("localStorage.getItem", "cc_role") ?? "";
Company = await JS.InvokeAsync<string>("localStorage.getItem", "cc_company") ?? "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || _loaded) return;
_loaded = true;

// Subscribe to AuthService changes
Auth.OnAuthStateChanged += Refresh;
var ctx = await UserCtx.RefreshAsync();
UserEmail = string.IsNullOrWhiteSpace(ctx.Email) ? "Guest" : ctx.Email;
UserRole = ctx.User?.Role ?? "";
CompanyName = ctx.User?.Company?.Name ?? "";

_hydrated = true;
StateHasChanged();
}
}
StateHasChanged();
}

private void Refresh() => InvokeAsync(StateHasChanged);

private async Task Logout()
{
await Auth.LogoutAsync();
private void Logout() => Nav.NavigateTo("/logout", forceLoad: true);


Nav.NavigateTo("/signin", forceLoad: true);
}

public void Dispose()
{
Auth.OnAuthStateChanged -= Refresh;
}
}
Loading
Loading