This commit is contained in:
2026-05-10 09:46:07 +12:00
parent cfc193b713
commit 2f2466ecac
81 changed files with 2571 additions and 413 deletions
+244
View File
@@ -0,0 +1,244 @@
<script lang="ts">
let { error, status } = $props();
const title = $derived(status === 404 ? 'Page Not Found' : 'Something Went Wrong');
const detail = $derived(
status === 404
? 'That route does not exist in the Hunter Premium Produce workspace. Check the address or return to login.'
: error instanceof Error
? error.message
: 'The workspace hit an unexpected error while loading this page.'
);
</script>
<svelte:head>
<title>{title} | Hunter Premium Produce</title>
</svelte:head>
<section class="error-stage">
<div class="error-card">
<div class="error-backdrop" aria-hidden="true">
<span class="glow glow-one"></span>
<span class="glow glow-two"></span>
<span class="grid-band"></span>
</div>
<div class="error-header">
<div class="brand-lockup">
<img class="brand-logo" src="/logo-hsf.png" alt="Hunter Premium Produce" />
<div>
<p class="eyebrow">Workspace Error</p>
<strong>Hunter Premium Produce</strong>
</div>
</div>
<span class="status-pill">{status}</span>
</div>
<div class="error-copy">
<p class="eyebrow">Route Response</p>
<h1>{title}</h1>
<p>{detail}</p>
</div>
<div class="error-actions">
<a class="primary-link" href="/">Return to Workspace</a>
<a class="secondary-link" href="/mix-calculator/new">Open Mix Calculator</a>
</div>
</div>
</section>
<style>
:global(body) {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(214, 234, 221, 0.86), transparent 36%),
linear-gradient(180deg, #f4f7f2 0%, #eef4ee 100%);
color: #1d3528;
font-family:
"Segoe UI",
system-ui,
sans-serif;
}
.error-stage {
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
}
.error-card {
position: relative;
overflow: hidden;
width: min(100%, 58rem);
padding: 2rem;
border: 1px solid rgba(32, 52, 41, 0.08);
border-radius: 1.5rem;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 24px 60px rgba(39, 63, 52, 0.12);
}
.error-backdrop {
position: absolute;
inset: 0;
pointer-events: none;
}
.glow {
position: absolute;
border-radius: 999px;
filter: blur(16px);
opacity: 0.8;
}
.glow-one {
top: -3rem;
right: -2rem;
width: 15rem;
height: 15rem;
background: rgba(160, 199, 124, 0.25);
}
.glow-two {
bottom: -4rem;
left: -3rem;
width: 18rem;
height: 18rem;
background: rgba(214, 166, 90, 0.16);
}
.grid-band {
position: absolute;
inset: auto 0 0;
height: 8rem;
background:
linear-gradient(rgba(33, 54, 42, 0.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(33, 54, 42, 0.07) 1px, transparent 1px);
background-size: 2rem 2rem;
mask-image: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.7));
}
.error-header,
.error-copy,
.error-actions {
position: relative;
z-index: 1;
}
.error-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.brand-lockup {
display: flex;
align-items: center;
gap: 0.9rem;
}
.brand-logo {
width: 3rem;
height: 3rem;
object-fit: contain;
}
.eyebrow {
margin: 0 0 0.22rem;
color: #5d7568;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.status-pill {
padding: 0.45rem 0.72rem;
border-radius: 999px;
background: rgba(31, 53, 40, 0.08);
color: #244331;
font-size: 0.84rem;
font-weight: 700;
}
.error-copy {
margin-top: 2.2rem;
max-width: 36rem;
}
.error-copy h1 {
margin: 0;
font-size: clamp(2.4rem, 6vw, 4.8rem);
line-height: 0.95;
letter-spacing: -0.04em;
}
.error-copy p:last-child {
margin: 1rem 0 0;
color: #587063;
font-size: 1rem;
line-height: 1.6;
}
.error-actions {
display: flex;
flex-wrap: wrap;
gap: 0.85rem;
margin-top: 2rem;
}
.primary-link,
.secondary-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 2.8rem;
padding: 0.7rem 1.15rem;
border-radius: 0.9rem;
font-weight: 700;
text-decoration: none;
transition:
transform 140ms ease,
box-shadow 140ms ease,
background 140ms ease;
}
.primary-link {
background: #274934;
color: #fff;
box-shadow: 0 14px 28px rgba(39, 73, 52, 0.22);
}
.secondary-link {
border: 1px solid rgba(39, 73, 52, 0.14);
background: rgba(255, 255, 255, 0.72);
color: #244331;
}
.primary-link:hover,
.secondary-link:hover {
transform: translateY(-1px);
}
@media (max-width: 640px) {
.error-stage {
padding: 1rem;
}
.error-card {
padding: 1.35rem;
border-radius: 1.15rem;
}
.error-header {
align-items: flex-start;
flex-direction: column;
}
.error-actions {
flex-direction: column;
}
}
</style>
+60 -2
View File
@@ -1,10 +1,13 @@
<script lang="ts">
import { api } from '$lib/api';
import { goto } from '$app/navigation';
import { clientSession, sessionHydrated } from '$lib/session';
import Skeleton from '$lib/components/Skeleton.svelte';
import type { DashboardSummary } from '$lib/types';
import { getWorkspaceHomeHref } from '$lib/workspace-access';
import packageInfo from '../../package.json';
import { Sunrise, Sun, Sunset, Moon } from 'lucide-svelte';
import { tick } from 'svelte';
type Segment = {
label: string;
@@ -32,8 +35,11 @@
let email = $state('');
let password = $state('');
let isLoggingIn = $state(false);
let postLoginRedirecting = $state(false);
let loginError = $state('');
let passwordInput: HTMLInputElement | null = null;
let emailInput = $state<HTMLInputElement | null>(null);
let passwordInput = $state<HTMLInputElement | null>(null);
let loginFocusArmed = $state(true);
const currentYear = new Date().getFullYear();
const appVersion = `v${packageInfo.version}`;
const releaseStage = 'Beta';
@@ -50,11 +56,17 @@
// system. The response is shape-compatible with the legacy client
// session, so the rest of the app continues to work unchanged.
const session = await api.internalLogin(email, password);
const targetHref = getWorkspaceHomeHref(session);
postLoginRedirecting = targetHref !== '/';
clientSession.set(session);
if (targetHref !== '/') {
await goto(targetHref, { replaceState: true });
}
} catch (error) {
loginError = error instanceof Error ? error.message : 'Unable to sign in';
triggerPasswordShake();
} finally {
postLoginRedirecting = false;
isLoggingIn = false;
}
}
@@ -84,6 +96,18 @@
);
}
$effect(() => {
if ($sessionHydrated && !$clientSession) {
if (loginFocusArmed && emailInput) {
loginFocusArmed = false;
tick().then(() => emailInput?.focus());
}
return;
}
loginFocusArmed = true;
});
function currency(value: number | null | undefined, digits = 2) {
if (value === null || value === undefined) {
return 'N/A';
@@ -363,7 +387,13 @@
<form class="signin-form auth-form" onsubmit={handleLogin}>
<label class="field">
<span>Email</span>
<input bind:value={email} type="email" autocomplete="username" placeholder="Email" autofocus />
<input
bind:this={emailInput}
bind:value={email}
type="email"
autocomplete="username"
placeholder="Email"
/>
</label>
<label class="field field-password" class:is-invalid={Boolean(loginError)}>
@@ -401,6 +431,34 @@
</div>
</div>
</section>
{:else if postLoginRedirecting}
<section class="auth-stage auth-stage-loading">
<div class="auth-card auth-card-loading">
<div class="auth-header">
<div class="client-logo-block">
<img class="hero-login-logo" src="/logo-hsf.png" alt="Lean 101" />
<div class="client-logo-copy">
<p class="eyebrow">Opening Workspace</p>
<strong>Hunter Premium Produce</strong>
<span>Applying your role permissions now</span>
</div>
</div>
</div>
<div class="auth-copy">
<h2>Preparing your workspace.</h2>
<p>Routing you directly to the first area your role is allowed to open.</p>
</div>
<div class="auth-loading-panel">
<span class="loading-pulse" aria-hidden="true"></span>
<div>
<strong>Applying Access Rules</strong>
<p>Dashboard access is skipped for roles that do not have permission.</p>
</div>
</div>
</div>
</section>
{:else}
<section class="dashboard-intro">
<div class="greeting-row">
+4 -11
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { getStoredClientSession, hasStoredClientSession } from '$lib/session';
import { canOpenDashboard, getWorkspaceHomeHref } from '$lib/workspace-access';
import type { DashboardSummary } from '$lib/types';
const EMPTY_SUMMARY: DashboardSummary = {
@@ -18,18 +20,9 @@ export function load({ fetch }) {
return { summary: Promise.resolve(EMPTY_SUMMARY) };
}
// Skip data fetching for sessions that lack any dashboard-eligible module
// — the backend would just return nulls anyway.
const session = getStoredClientSession();
const permissions = session?.module_permissions ?? {};
const hasAnyDashboardData =
session?.role === 'admin' ||
permissions.dashboard ||
permissions.raw_materials ||
permissions.mix_master ||
permissions.products;
if (!hasAnyDashboardData) {
return { summary: Promise.resolve(EMPTY_SUMMARY) };
if (!canOpenDashboard(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
return {
+2 -2
View File
@@ -4,8 +4,8 @@
let { data } = $props();
let email = $state('admin@lean101.local');
let password = $state('lean101-admin');
let email = $state('');
let password = $state('');
let isLoggingIn = $state(false);
let loginError = $state('');
+10 -1
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { hasStoredAdminSession, hasStoredClientSession } from '$lib/session';
import { getStoredClientSession, hasStoredAdminSession, hasStoredClientSession } from '$lib/session';
import { canOpenClientAccess, getWorkspaceHomeHref } from '$lib/workspace-access';
function emptyPayload() {
return {
@@ -21,6 +23,13 @@ export async function load({ fetch }) {
return emptyPayload();
}
if (hasStoredClientSession()) {
const session = getStoredClientSession();
if (session && !canOpenClientAccess(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
}
try {
const [clients, exportPreview] = await Promise.all([api.clientAccess(fetch), api.clientAccessExport(fetch)]);
return { clients, exportPreview };
+5 -1
View File
@@ -2,6 +2,7 @@ import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { featureFlags } from '$lib/features';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { canOpenMixCalculator, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!featureFlags.mixCalculatorSessionHistory) {
@@ -15,10 +16,13 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canOpenMixCalculator(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
return {
sessions: hasModuleAccess(session, 'mix_calculator') ? await api.mixCalculatorSessions(fetch) : []
sessions: hasModuleAccess(session, 'mix_calculator') || session?.role === 'internal' ? await api.mixCalculatorSessions(fetch) : []
};
} catch {
return {
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { canCreateMixSession, canOpenMixCalculator, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ params, fetch }) {
if (!hasStoredClientSession()) {
@@ -10,20 +12,17 @@ export async function load({ params, fetch }) {
}
const session = getStoredClientSession();
const canView = hasModuleAccess(session, 'mix_calculator');
const canEdit = hasModuleAccess(session, 'mix_calculator', 'edit');
const canView = canOpenMixCalculator(session);
const canEdit = canCreateMixSession(session);
if (!canView) {
return {
session: null,
options: { clients: [], products: [] }
};
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
const [savedSession, options] = await Promise.all([
api.mixCalculatorSession(Number(params.id), fetch),
canEdit ? api.mixCalculatorOptions(fetch) : Promise.resolve({ clients: [], products: [] })
canEdit || session?.role === 'internal' ? api.mixCalculatorOptions(fetch) : Promise.resolve({ clients: [], products: [] })
]);
return {
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { canOpenMixCalculator, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ params, fetch }) {
if (!hasStoredClientSession()) {
@@ -10,10 +12,8 @@ export async function load({ params, fetch }) {
const session = getStoredClientSession();
if (!hasModuleAccess(session, 'mix_calculator')) {
return {
session: null
};
if (!canOpenMixCalculator(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { canCreateMixSession, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -9,10 +11,13 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canCreateMixSession(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
return {
options: hasModuleAccess(session, 'mix_calculator', 'edit')
options: hasModuleAccess(session, 'mix_calculator', 'edit') || session?.role === 'internal'
? await api.mixCalculatorOptions(fetch)
: { clients: [], products: [] }
};
+6 -1
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { api } from '$lib/api';
import { canOpenMixMaster, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -9,10 +11,13 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canOpenMixMaster(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
return {
mixes: hasModuleAccess(session, 'mix_master') ? await api.mixes(fetch) : []
mixes: hasModuleAccess(session, 'mix_master') || session?.role === 'internal' ? await api.mixes(fetch) : []
};
} catch {
return {
+5 -7
View File
@@ -1,6 +1,7 @@
import { error } from '@sveltejs/kit';
import { error, redirect } from '@sveltejs/kit';
import { api } from '$lib/api';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { canOpenMixMaster, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ params, fetch }) {
const mixId = Number(params.id);
@@ -17,17 +18,14 @@ export async function load({ params, fetch }) {
}
const session = getStoredClientSession();
if (!hasModuleAccess(session, 'mix_master')) {
return {
mix: null,
rawMaterials: []
};
if (!canOpenMixMaster(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
const [mix, rawMaterials] = await Promise.all([
api.mix(mixId, fetch),
hasModuleAccess(session, 'raw_materials') ? api.rawMaterials(fetch) : Promise.resolve([])
hasModuleAccess(session, 'raw_materials') || session?.role === 'internal' ? api.rawMaterials(fetch) : Promise.resolve([])
]);
return {
+9 -1
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { api } from '$lib/api';
import { canCreateMixWorksheet, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -9,10 +11,16 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canCreateMixWorksheet(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
return {
rawMaterials: hasModuleAccess(session, 'mix_master') && hasModuleAccess(session, 'raw_materials') ? await api.rawMaterials(fetch) : []
rawMaterials:
(hasModuleAccess(session, 'mix_master') && hasModuleAccess(session, 'raw_materials')) || session?.role === 'internal'
? await api.rawMaterials(fetch)
: []
};
} catch {
return {
+7 -2
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { api } from '$lib/api';
import { canOpenProducts, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -10,11 +12,14 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canOpenProducts(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
const [products, productCosts] = await Promise.all([
hasModuleAccess(session, 'products') ? api.products(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') ? api.productCosts(fetch) : Promise.resolve([])
hasModuleAccess(session, 'products') || session?.role === 'internal' ? api.products(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') || session?.role === 'internal' ? api.productCosts(fetch) : Promise.resolve([])
]);
return {
products,
+9 -4
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { api } from '$lib/api';
import { canOpenRawMaterials, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -12,13 +14,16 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canOpenRawMaterials(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
const [rawMaterials, mixes, products, productCosts] = await Promise.all([
hasModuleAccess(session, 'raw_materials') ? api.rawMaterials(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'mix_master') ? api.mixes(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') ? api.products(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') ? api.productCosts(fetch) : Promise.resolve([])
hasModuleAccess(session, 'raw_materials') || session?.role === 'internal' ? api.rawMaterials(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'mix_master') || session?.role === 'internal' ? api.mixes(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') || session?.role === 'internal' ? api.products(fetch) : Promise.resolve([]),
hasModuleAccess(session, 'products') || session?.role === 'internal' ? api.productCosts(fetch) : Promise.resolve([])
]);
return {
+16
View File
@@ -0,0 +1,16 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasStoredClientSession } from '$lib/session';
import { canOpenReporting, getWorkspaceHomeHref } from '$lib/workspace-access';
export function load() {
if (!hasStoredClientSession()) {
return {};
}
const session = getStoredClientSession();
if (session && !canOpenReporting(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
return {};
}
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it, vi } from 'vitest';
const apiMocks = vi.hoisted(() => ({
dashboardSummary: vi.fn()
}));
const sessionMocks = vi.hoisted(() => ({
getStoredClientSession: vi.fn(),
hasStoredClientSession: vi.fn(),
hasModuleAccess: vi.fn(),
hasPermission: vi.fn()
}));
vi.mock('$lib/api', () => ({
api: apiMocks
}));
vi.mock('$lib/session', () => sessionMocks);
import { load } from './+page';
describe('root route access', () => {
it('redirects operations users away from the dashboard route', () => {
sessionMocks.hasStoredClientSession.mockReturnValue(true);
sessionMocks.getStoredClientSession.mockReturnValue({
role: 'internal',
role_name: 'Operations',
permissions: ['view_mix_calculator']
});
sessionMocks.hasPermission.mockImplementation((session, key) => session?.permissions?.includes(key) ?? false);
sessionMocks.hasModuleAccess.mockReturnValue(false);
expect(() => load({ fetch: vi.fn() as typeof fetch })).toThrow(
expect.objectContaining({ status: 307, location: '/mix-calculator/new' })
);
});
it('loads the dashboard summary for users with dashboard access', () => {
sessionMocks.hasStoredClientSession.mockReturnValue(true);
sessionMocks.getStoredClientSession.mockReturnValue({ role: 'internal', permissions: ['view_dashboard'] });
sessionMocks.hasPermission.mockImplementation((session, key) => session?.permissions?.includes(key) ?? false);
apiMocks.dashboardSummary.mockResolvedValue({ ok: true });
const result = load({ fetch: vi.fn() as typeof fetch });
expect(apiMocks.dashboardSummary).toHaveBeenCalled();
expect(result).toHaveProperty('summary');
});
});
+5
View File
@@ -1,5 +1,7 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasModuleAccess, hasStoredClientSession } from '$lib/session';
import { api } from '$lib/api';
import { canOpenScenarios, getWorkspaceHomeHref } from '$lib/workspace-access';
export async function load({ fetch }) {
if (!hasStoredClientSession()) {
@@ -9,6 +11,9 @@ export async function load({ fetch }) {
}
const session = getStoredClientSession();
if (!canOpenScenarios(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
try {
return {
@@ -29,7 +29,6 @@
...$clientSession!,
name: updated.name,
email: updated.email,
token: updated.token ?? $clientSession!.token,
});
toast.dismiss(tid);
toast.success('Profile updated');
+20
View File
@@ -0,0 +1,20 @@
import { redirect } from '@sveltejs/kit';
import { getStoredClientSession, hasStoredClientSession } from '$lib/session';
import { canOpenSettings, getWorkspaceHomeHref } from '$lib/workspace-access';
export function load() {
if (!hasStoredClientSession()) {
return {};
}
const session = getStoredClientSession();
if (!session) {
return {};
}
if (!canOpenSettings(session)) {
throw redirect(307, getWorkspaceHomeHref(session));
}
return {};
}
@@ -0,0 +1,35 @@
import { describe, expect, it, vi } from 'vitest';
const sessionMocks = vi.hoisted(() => ({
getStoredClientSession: vi.fn(),
hasStoredClientSession: vi.fn(),
hasModuleAccess: vi.fn(),
hasPermission: vi.fn()
}));
vi.mock('$lib/session', () => sessionMocks);
import { load } from './+page';
describe('settings route access', () => {
it('allows users with settings access', () => {
sessionMocks.hasStoredClientSession.mockReturnValue(true);
sessionMocks.getStoredClientSession.mockReturnValue({ role: 'internal', permissions: ['view_settings'] });
sessionMocks.hasPermission.mockImplementation((session, key) => session?.permissions?.includes(key) ?? false);
expect(load()).toEqual({});
});
it('redirects users without settings access to their allowed home route', () => {
sessionMocks.hasStoredClientSession.mockReturnValue(true);
sessionMocks.getStoredClientSession.mockReturnValue({
role: 'internal',
role_name: 'Operations',
permissions: ['view_mix_calculator']
});
sessionMocks.hasPermission.mockImplementation((session, key) => session?.permissions?.includes(key) ?? false);
sessionMocks.hasModuleAccess.mockReturnValue(false);
expect(() => load()).toThrow(expect.objectContaining({ status: 307, location: '/mix-calculator/new' }));
});
});