Updates
This commit is contained in:
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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('');
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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: [] }
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
@@ -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' }));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user