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
20 changes: 7 additions & 13 deletions app/admin/modules/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { redirect } from 'next/navigation';
import { skossCoreRoutes } from '@/lib/application-planes';
import { moduleRegistry } from '@/lib/modules';
import { getCurrentUserContext } from '@/lib/server/auth';
import { readAppData } from '@/lib/server/persistence';
import { requireAdminPlaneAccess } from '@/lib/server/application-access';
import { readPersistence } from '@/lib/server/persistence';
import { updateModuleRegistryAction } from '@/lib/server/actions';

export default async function AdminModulesPage({ searchParams }: { searchParams?: Promise<{ saved?: string }> }) {
const [userContext, data, params] = await Promise.all([getCurrentUserContext(), readAppData(), searchParams]);

if (!userContext.currentUser) {
redirect('/login?redirectTo=/admin/modules');
}

if (!userContext.canManageSettings) {
redirect('/');
}
await requireAdminPlaneAccess(skossCoreRoutes.adminModules);
const [persistence, params] = await Promise.all([readPersistence(), searchParams]);
const moduleStates = persistence.instance.getInstanceState().moduleStates ?? {};

return (
<div className="page-stack">
Expand All @@ -25,7 +19,7 @@ export default async function AdminModulesPage({ searchParams }: { searchParams?
{params?.saved === 'modules' ? <p className="inline-success">Module settings updated.</p> : null}
<form action={updateModuleRegistryAction} className="panel page-stack">
{moduleRegistry.map((module) => {
const enabled = module.required || data.instance.moduleStates?.[module.id] !== false;
const enabled = module.required || moduleStates[module.id] !== false;
return (
<label key={module.id} className="checkbox-row">
<input
Expand Down
18 changes: 5 additions & 13 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { getCurrentUserContext } from '@/lib/server/auth';
import { skossCoreRoutes } from '@/lib/application-planes';
import { requireAdminPlaneAccess } from '@/lib/server/application-access';

export default async function AdminHomePage() {
const userContext = await getCurrentUserContext();

if (!userContext.currentUser) {
redirect('/login?redirectTo=/admin');
}

if (!userContext.canManageSettings) {
redirect('/');
}
await requireAdminPlaneAccess(skossCoreRoutes.admin);

return (
<div className="page-stack">
Expand All @@ -21,11 +13,11 @@ export default async function AdminHomePage() {
<p className="lede">Structural setup, module control, imports, and system configuration stay on the admin plane.</p>
</section>
<section className="grid-cards">
<Link href="/admin/setup" className="panel-link">
<Link href={skossCoreRoutes.adminSetup} className="panel-link">
<h2>Structural setup</h2>
<p>Users, suppliers, materials, recipes, imports, and business fine tuning.</p>
</Link>
<Link href="/admin/modules" className="panel-link">
<Link href={skossCoreRoutes.adminModules} className="panel-link">
<h2>Module control</h2>
<p>Core-required and optional module defaults for this instance.</p>
</Link>
Expand Down
22 changes: 7 additions & 15 deletions app/admin/setup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { skossBootstrapRoutes, skossCoreRoutes } from '@/lib/application-planes';
import {
formatCurrency,
formatDateLabel,
Expand All @@ -19,8 +19,8 @@ import {
updateUserAction,
resetDemoWorkspaceAction,
} from '@/lib/server/actions';
import { requireAdminPlaneAccess } from '@/lib/server/application-access';
import { getSetupWorkspace } from '@/lib/server/demo-data';
import { getCurrentUserContext } from '@/lib/server/auth';
import { getServerTranslator } from '@/lib/i18n/server';
import { getVisibleWorkspacesForRole } from '@/lib/workspaces';
import { ThemeSwitcher } from '@/components/theme-switcher';
Expand Down Expand Up @@ -70,7 +70,7 @@ type SetupSearchParams = {

function buildSetupHref(params: Record<string, string | undefined>) {
return {
pathname: '/admin/setup',
pathname: skossCoreRoutes.adminSetup,
query: Object.fromEntries(
Object.entries(params).filter(([, value]) => Boolean(value)),
),
Expand Down Expand Up @@ -168,24 +168,16 @@ export default async function SetupPage({
}: {
searchParams?: Promise<SetupSearchParams>;
}) {
const [data, params, { t, locale, term }, userContext] = await Promise.all([
const userContext = await requireAdminPlaneAccess(skossCoreRoutes.adminSetup);
const [data, params, { t, locale, term }] = await Promise.all([
getSetupWorkspace(),
searchParams,
getServerTranslator(),
getCurrentUserContext(),
]);
const today = new Date().toISOString().slice(0, 10);
const runtimeMode = getRuntimeMode();
const canResetDemoWorkspace = isNonProductionMode();

if (!userContext.currentUser) {
redirect('/login?redirectTo=/admin/setup');
}

if (!userContext.canManageSettings) {
redirect('/');
}

const editingSupplier = params?.supplier
? (data.suppliers.find((supplier) => supplier.id === params.supplier) ??
null)
Expand Down Expand Up @@ -353,7 +345,7 @@ export default async function SetupPage({
{ key: 'price-history', label: t('setup.sections.priceHistory') },
];
const buildSectionLink = (section: (typeof sectionNavItems)[number]['key']) => ({
pathname: '/admin/setup',
pathname: skossCoreRoutes.adminSetup,
query: { section },
hash: section,
});
Expand Down Expand Up @@ -455,7 +447,7 @@ export default async function SetupPage({
))}
</ul>
<div className="inline-action-row">
<Link href="/bootstrap?step=1" className="button-secondary compact-button">
<Link href={skossBootstrapRoutes.bootstrapStart} className="button-secondary compact-button">
Resume guided activation
</Link>
<a href="#suppliers" className="button-secondary compact-button">
Expand Down
5 changes: 3 additions & 2 deletions app/customers/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from 'next/link';
import { skossCoreRoutes, skossinaRoutes } from '@/lib/application-planes';
import { formatDateLabel, formatStatusLabel } from '@/lib/domain/formatters';
import { createCustomerAction, updateCustomerAction } from '@/lib/server/actions';
import { getCustomersWorkspace } from '@/lib/server/demo-data';
Expand Down Expand Up @@ -42,10 +43,10 @@ export default async function CustomersPage({
<p>{t('customers.description')}</p>
</div>
<div className="inline-action-row">
<Link href="/admin/setup#imports" className="button-secondary">
<Link href={skossCoreRoutes.adminSetupImports} className="button-secondary">
<span>{t('customers.actions.importCsv')}</span>
</Link>
<Link href="/orders" className="button-secondary">
<Link href={skossinaRoutes.orders} className="button-secondary">
<ArrowRightIcon className="button-icon button-icon-reverse" />
<span>{t('common.backToOrders')}</span>
</Link>
Expand Down
5 changes: 3 additions & 2 deletions app/entry/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { LanguageSwitcher } from '@/components/language-switcher';
import { skossBootstrapRoutes, skossinaRoutes } from '@/lib/application-planes';
import {
launchDemoModeAction,
recoverLocalAdminAccessAction,
Expand Down Expand Up @@ -81,7 +82,7 @@ export default async function EntryGatewayPage({
const state = await detectInstanceGatewayState(data);

if (currentUser && state.hasAdminUser && state.hasInstance && !state.onboardingIncomplete) {
redirect('/');
redirect(skossinaRoutes.home);
}
const copy = getEntryCopy(locale);
const runtimeMode = getRuntimeMode();
Expand Down Expand Up @@ -126,7 +127,7 @@ export default async function EntryGatewayPage({
<h2>{copy.newTitle}</h2>
<p>{copy.newBody}</p>
{state.canRunBootstrap ? (
<Link className="button-primary" href="/bootstrap?step=1">
<Link className="button-primary" href={skossBootstrapRoutes.bootstrapStart}>
{copy.newAction}
</Link>
) : (
Expand Down
7 changes: 4 additions & 3 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Link from 'next/link';
import { skossBootstrapRoutes } from '@/lib/application-planes';
import { loginAction } from '@/lib/server/actions';
import { getServerTranslator } from '@/lib/i18n/server';
import { readAppData } from '@/lib/server/persistence';
import { detectInstanceGatewayState } from '@/lib/server/instance-entry';
import Link from 'next/link';

export default async function LoginPage({
searchParams,
Expand Down Expand Up @@ -52,11 +53,11 @@ export default async function LoginPage({
</datalist>
<button type="submit" className="button-primary">{t('login.submit')}</button>
<p className="helper-text no-margin">
Need a different path? <Link href="/entry" className="inline-link">Back to entry gateway</Link> for new setup, restore, or demo mode.
Need a different path? <Link href={skossBootstrapRoutes.entry} className="inline-link">Back to entry gateway</Link> for new setup, restore, or demo mode.
</p>
{gatewayState.canRunBootstrap ? (
<p className="helper-text no-margin">
Need to initialize this instance first? <Link href="/bootstrap?step=1" className="inline-link">Run first-use wizard</Link>
Need to initialize this instance first? <Link href={skossBootstrapRoutes.bootstrapStart} className="inline-link">Run first-use wizard</Link>
</p>
) : null}
</form>
Expand Down
23 changes: 12 additions & 11 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@ import { redirect } from 'next/navigation';
import { ActivityFeed } from '@/components/activity-feed';
import { formatDateLabel } from '@/lib/domain/formatters';
import { getPresetExperience, type WorkspaceLinkKey } from '@/lib/business-presets';
import { getWorkspaceSummary } from '@/lib/server/demo-data';
import { buildWorkspaceSummary } from '@/lib/server/demo-data';
import { getCurrentUserContext } from '@/lib/server/auth';
import { detectInstanceGatewayState, shouldRouteToEntryGateway } from '@/lib/server/instance-entry';
import { getServerTranslator } from '@/lib/i18n/server';
import { readAppData } from '@/lib/server/persistence';
import { skossBootstrapRoutes, skossCoreRoutes, skossinaRoutes, type AppPlaneRoute } from '@/lib/application-planes';
import { ArrowRightIcon, CustomersIcon, HandoffIcon, OrdersIcon, ProductionIcon, SetupIcon, SparklesIcon, TimelineIcon } from '@/components/ui-icons';

type QuickLink = {
href: Parameters<typeof Link>[0]['href'];
href: AppPlaneRoute;
title: string;
description: string;
icon: typeof OrdersIcon;
};

export default async function HomePage() {
const [summary, { t, locale, preset }, data, userContext] = await Promise.all([
getWorkspaceSummary(),
const [{ t, locale, preset }, data, userContext] = await Promise.all([
getServerTranslator(),
readAppData(),
getCurrentUserContext(),
]);
const summary = buildWorkspaceSummary(data);
const { currentUser, visibleWorkspaces, homeWorkspace } = userContext;
const gatewayState = await detectInstanceGatewayState(data);

Expand All @@ -39,37 +40,37 @@ export default async function HomePage() {

const quickLinkMap: Record<WorkspaceLinkKey, QuickLink> = {
timeline: {
href: '/timeline',
href: skossinaRoutes.timeline,
title: t('home.quickLinks.timeline.title'),
description: t('home.quickLinks.timeline.description'),
icon: TimelineIcon,
},
orders: {
href: '/orders',
href: skossinaRoutes.orders,
title: t('home.quickLinks.orders.title'),
description: t('home.quickLinks.orders.description'),
icon: OrdersIcon,
},
customers: {
href: '/customers',
href: skossinaRoutes.customers,
title: t('home.quickLinks.customers.title'),
description: t('home.quickLinks.customers.description'),
icon: CustomersIcon,
},
production: {
href: '/production',
href: skossinaRoutes.production,
title: t('home.quickLinks.production.title'),
description: t('home.quickLinks.production.description'),
icon: ProductionIcon,
},
handoff: {
href: '/handoff',
href: skossinaRoutes.handoff,
title: t('home.quickLinks.handoff.title'),
description: t('home.quickLinks.handoff.description'),
icon: HandoffIcon,
},
admin: {
href: '/admin/setup',
href: skossCoreRoutes.adminSetup,
title: t('home.quickLinks.setup.title'),
description: t('home.quickLinks.setup.description'),
icon: SetupIcon,
Expand All @@ -84,7 +85,7 @@ export default async function HomePage() {
const orderedQuickLinks = orderedQuickLinkKeys.map((key) => quickLinkMap[key]);

if (!summary.preferences.onboardingCompleted) {
redirect('/bootstrap?step=1');
redirect(skossBootstrapRoutes.bootstrapStart);
}

return (
Expand Down
3 changes: 2 additions & 1 deletion app/setup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { redirect } from 'next/navigation';
import { skossCoreRoutes } from '@/lib/application-planes';

export default function LegacySetupRedirectPage() {
redirect('/admin/setup');
redirect(skossCoreRoutes.adminSetup);
}
15 changes: 9 additions & 6 deletions components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import type { ReactNode } from 'react';
import { headers } from 'next/headers';
import { PrimaryNav } from '@/components/primary-nav';
import { UserMenu } from '@/components/user-menu';
import { getApplicationPlane, skossinaRoutes } from '@/lib/application-planes';
import { getServerTranslator } from '@/lib/i18n/server';
import { readAppData } from '@/lib/server/persistence';
import { readPersistence } from '@/lib/server/persistence';
import { getCurrentUserContext } from '@/lib/server/auth';
import { getRuntimeModeLabel, isNonProductionMode } from '@/lib/server/runtime-mode';

export async function AppShell({ children }: { children?: ReactNode }) {
const [{ t }, data] = await Promise.all([getServerTranslator(), readAppData()]);
const [{ t }, persistence] = await Promise.all([getServerTranslator(), readPersistence()]);
const data = persistence.raw;
const workspace = persistence.instance.getWorkspace();
const { currentUser, visibleWorkspaces } = await getCurrentUserContext(data);
const showNonProductionBanner = isNonProductionMode();
const runtimeModeLabel = getRuntimeModeLabel();
Expand All @@ -19,9 +22,9 @@ export async function AppShell({ children }: { children?: ReactNode }) {
?? headerStore.get('x-invoke-path')
?? headerStore.get('x-matched-path')
?? '';
const isBootstrapRoute = requestPath.startsWith('/bootstrap');
const plane = getApplicationPlane(requestPath);

if (isBootstrapRoute) {
if (plane === 'bootstrap') {
return (
<div className="shell onboarding-shell">
{showNonProductionBanner ? (
Expand All @@ -42,9 +45,9 @@ export async function AppShell({ children }: { children?: ReactNode }) {
</div>
) : null}
<header className="shell-header">
<Link href="/" className="brand-block" aria-label={t('shell.homeAria')}>
<Link href={skossinaRoutes.home} className="brand-block" aria-label={t('shell.homeAria')}>
<span className="brand-kicker">SKOSS</span>
<strong>{data.workspace.name}</strong>
<strong>{workspace.name}</strong>
<span className="brand-subtitle">{t('shell.brandTitle')}</span>
</Link>
<div className="shell-controls">
Expand Down
Loading
Loading