-
+
diff --git a/app/bootstrap/page.tsx b/app/bootstrap/page.tsx
index 5f45d79..514fee9 100644
--- a/app/bootstrap/page.tsx
+++ b/app/bootstrap/page.tsx
@@ -1,13 +1,11 @@
import { redirect } from 'next/navigation';
-import { CsvImportCard } from '@/components/setup/csv-import-card';
-import { TeamRosterBuilder } from '@/components/setup/team-roster-builder';
import { TimezoneSelect } from '@/components/setup/timezone-select';
-import { saveBootstrapStepAction, importCsvEntitiesAction } from '@/lib/server/actions';
+import { saveBootstrapStepAction } from '@/lib/server/actions';
import { readAppData } from '@/lib/server/persistence';
import { detectInstanceGatewayState } from '@/lib/server/instance-entry';
-const totalSteps = 8;
-const requiredSteps = new Set([1, 2]);
+const totalSteps = 3;
+const requiredSteps = new Set([1, 2, 3]);
function bootstrapStep(step: number) {
return Math.max(1, Math.min(totalSteps, step));
@@ -35,19 +33,19 @@ export default async function BootstrapPage({
-
First-time setup
-
Start your kitchen workspace
+
Required activation
+
Create a clean SKOSS instance
- Guided activation first, then optional setup. Required steps are marked so you can launch fast and keep configuring later.
+ Activation creates the owner account, business identity, language, data mode, and operating profile. Team, customers, products, recipes, delivery, shifts, and first orders continue after sign-in.
Step {step} of {totalSteps}
-
+
- Required now: steps 1-2 (admin account and workspace basics). Optional and skippable: steps 3-7.
+ Demo and restore are separate paths from the entry gateway. This flow launches a real instance without fictional operational records.
+ Minimum viable launch expects business profile, customers, products, and initial orders to be ready or intentionally deferred. Recipes, shifts, and delivery can mature later.
+
+
+ {minimumReady ? 'Viable' : 'Needs decisions'}
+
+
Skipped sections: {skippedCount}. Keep this list visible until skipped work has a real owner.
+
+
+ );
+}
diff --git a/docs/bootstrap-v2-onboarding.md b/docs/bootstrap-v2-onboarding.md
new file mode 100644
index 0000000..374736c
--- /dev/null
+++ b/docs/bootstrap-v2-onboarding.md
@@ -0,0 +1,56 @@
+# Bootstrap v2 and Operational Onboarding
+
+SKOSS first run is split into two layers.
+
+## Required activation
+
+Activation is short and mandatory. It creates the minimum real instance shell:
+
+- language
+- clean real instance/data mode
+- basic business identity
+- owner/admin account
+- business preset or operational profile
+- instance creation confirmation
+
+Demo mode and restore mode remain separate choices from the entry gateway. A clean real activation must not silently copy demo customers, products, orders, activities, recurring templates, WIP, destinations, suppliers, or demo team users.
+
+## Resumable operational onboarding
+
+After activation, admins continue from a section checklist. Each section can be configured now, skipped temporarily, marked ready, and returned to later.
+
+Tracked section states are:
+
+- `not_started`
+- `in_progress`
+- `ready`
+- `skipped`
+
+`skipped` means intentionally deferred. It is not the same as complete.
+
+Current sections are:
+
+- business profile
+- team and roles
+- shifts
+- customers
+- products
+- recipes
+- delivery/destination methods
+- initial orders
+
+## Future data depth
+
+Business data can grow to include name, description, logo, contact, address, website, hours, and fulfillment methods.
+
+Team data can grow to include name, login, phone, email, practical roles, active state, and shift assignment. Avoid unnecessary sensitive personal fields by default.
+
+Customer data can grow to include person/business type, alias, organization, contact person, phones, email, address, notes, and fulfillment preferences.
+
+Product data can grow to include name, category, description, unit, presentation, variant, active state, optional price, and future recipe/component relations.
+
+Imports should use a reusable CSV mapping architecture for customers, team, products, and later initial orders. This pass only shows honest placeholders where import support belongs; it does not implement format-specific importers.
+
+## Non-goals for this pass
+
+This pass does not implement full HR, product, recipe, procurement, inventory, or delivery systems. It establishes the first-run information architecture and clean-instance state model.
diff --git a/docs/first-deploy-rehearsal.md b/docs/first-deploy-rehearsal.md
index c28a6a1..02da893 100644
--- a/docs/first-deploy-rehearsal.md
+++ b/docs/first-deploy-rehearsal.md
@@ -41,16 +41,16 @@ If the runtime mode is unclear in `/entry`, stop the rehearsal and inspect envir
- Failure signs: demo mode appears active when preparing a real rehearsal, or guided activation is unavailable on a clean instance.
2. Run `/bootstrap`.
- - Expected: guided activation allows admin creation and workspace basics. Optional setup can remain incomplete.
- - Failure signs: bootstrap is locked even though the instance is clean, or setup cannot create/recover an admin user.
+ - Expected: required activation creates only the owner/admin account, language, clean real data mode, business identity, preset/profile, and confirmation. Operational onboarding continues after sign-in.
+ - Failure signs: bootstrap is locked even though the instance is clean, setup cannot create/recover an admin user, or the real path looks like a demo-data wizard.
3. Create or recover admin access.
- Expected: a working admin account can sign in after activation.
- Failure signs: no active admin user is detected, login loops back to `/entry`, or recovery is needed in `production` mode.
-4. Confirm business and workspace setup in `/admin/setup`.
- - Expected: business identity, workspace preferences, customers, and basic catalog setup are visible without requiring procurement or inventory setup.
- - Failure signs: a user must complete suppliers, raw materials, recipes, costing, or inventory before order capture.
+4. Confirm operational onboarding in `/admin/setup?section=onboarding`.
+ - Expected: the checklist shows business, team, shifts, customers, products, recipes, delivery, and initial orders with section status, configure actions, skip/return-later behavior, and honest import placeholders.
+ - Failure signs: skipped sections appear complete, imports pretend to be implemented, or order capture is blocked by suppliers, raw materials, recipes, costing, or inventory.
5. Confirm customers in `/customers`.
- Expected: existing customers can be reviewed or a first real customer can be added.
@@ -125,7 +125,7 @@ The first local deploy rehearsal should not attempt to validate:
- Confirm the selected runtime mode and persistence mode are documented for the deployment target.
- Define a concrete manual backup/export procedure until a user-facing export flow exists.
- Verify admin recovery expectations in `production` mode.
-- Verify demo seed data is not mixed with real rehearsal records.
+- Verify demo seed data is not mixed with real rehearsal records. A clean real activation should keep only the activated owner/admin account and structural preferences, with empty customers, products, orders, recurring templates, WIP, activities, destinations, suppliers, recipes, and demo team users.
## Recommended Next PR
diff --git a/docs/first-entry-and-onboarding.md b/docs/first-entry-and-onboarding.md
index 8444335..2e6fd35 100644
--- a/docs/first-entry-and-onboarding.md
+++ b/docs/first-entry-and-onboarding.md
@@ -46,6 +46,7 @@ A lightweight `instance` object is persisted in the store:
- `roles`
- `shifts`
- `optionalImports`
+- `operationalOnboarding` section statuses for resumable real-data setup
- `operatorOnboardingByUserId` (future role onboarding tracking)
Detection is intentionally simple and reversible.
@@ -74,17 +75,13 @@ Current routing behavior:
Goal: predictable routing with small condition sets.
-## Admin setup flow
+## Bootstrap v2 activation and onboarding
-From gateway, **Start a new kitchen** routes to setup onboarding entry (`/setup?section=business-setup`).
+From gateway, **Start a new kitchen** routes to required activation (`/bootstrap`). Activation is intentionally short: owner/admin account, language, clean real data mode, business identity, preset/profile, and confirmation.
-Current onboarding progress support includes:
+After activation, admins continue from `/admin/setup?section=onboarding`. Operational onboarding is section-based and resumable. Section statuses are `not_started`, `in_progress`, `ready`, and `skipped`. Skipped sections remain visible as deferred work, not completed work.
-- step flags in `instance.onboardingProgress`
-- save progress by persisted store updates
-- resume via deterministic setup route
-
-Detailed multi-step wizard UX can be layered in later PRs without changing the state contract.
+Tracked operational sections are business, team, shifts, customers, products, recipes, delivery/destinations, and initial orders.
## Demo mode flow
diff --git a/lib/domain/types.ts b/lib/domain/types.ts
index a9a651e..a827ffe 100644
--- a/lib/domain/types.ts
+++ b/lib/domain/types.ts
@@ -7,6 +7,16 @@ export type WorkspaceSurface = 'home' | 'timeline' | 'orders' | 'customers' | 'p
export type CustomerContactMethod = 'phone' | 'email' | 'whatsapp';
export type EnvironmentType = 'dev' | 'demo' | 'pilot' | 'production';
export type OnboardingStatus = 'not_started' | 'in_progress' | 'completed';
+export type OnboardingSectionStatus = 'not_started' | 'in_progress' | 'ready' | 'skipped';
+export type OperationalOnboardingSectionKey =
+ | 'business'
+ | 'team'
+ | 'customers'
+ | 'products'
+ | 'recipes'
+ | 'delivery'
+ | 'shifts'
+ | 'initialOrders';
export type OrderStatus = 'draft' | 'active' | 'changed' | 'cancelled' | 'completed';
export type OrderLineStatus = 'pending' | 'in_progress' | 'done' | 'cancelled';
@@ -80,6 +90,14 @@ export interface OnboardingProgress {
optionalImports: boolean;
}
+export type OperationalOnboardingSections = Record<
+ OperationalOnboardingSectionKey,
+ {
+ status: OnboardingSectionStatus;
+ updatedAt?: string;
+ }
+>;
+
export interface InstanceState {
initialized: boolean;
onboardingStatus: OnboardingStatus;
@@ -88,6 +106,7 @@ export interface InstanceState {
backupHintAvailable: boolean;
lastRestoreAt?: string;
onboardingProgress: OnboardingProgress;
+ operationalOnboarding?: OperationalOnboardingSections;
operatorOnboardingByUserId?: Record;
moduleStates?: Record;
}
diff --git a/lib/server/actions/admin-core.ts b/lib/server/actions/admin-core.ts
index 11bc9ee..434cfec 100644
--- a/lib/server/actions/admin-core.ts
+++ b/lib/server/actions/admin-core.ts
@@ -37,19 +37,68 @@ import {
import { moduleRegistry } from '@/lib/modules';
import { getCurrentUserContext } from '@/lib/server/auth';
import { isNonProductionMode } from '@/lib/server/runtime-mode';
-import type { AppData } from '@/lib/domain/types';
+import type { AppData, OnboardingSectionStatus, OperationalOnboardingSectionKey, OperationalOnboardingSections } from '@/lib/domain/types';
function quoteLabel(label: string) {
return `"${label}"`;
}
const persistence = getPersistenceGateway();
+const operationalOnboardingSections: OperationalOnboardingSectionKey[] = [
+ 'business',
+ 'team',
+ 'customers',
+ 'products',
+ 'recipes',
+ 'delivery',
+ 'shifts',
+ 'initialOrders',
+];
+const operationalOnboardingStatuses: OnboardingSectionStatus[] = ['not_started', 'in_progress', 'ready', 'skipped'];
+
+function buildFallbackOperationalOnboarding(): OperationalOnboardingSections {
+ return {
+ business: { status: 'not_started' },
+ team: { status: 'not_started' },
+ customers: { status: 'not_started' },
+ products: { status: 'not_started' },
+ recipes: { status: 'not_started' },
+ delivery: { status: 'not_started' },
+ shifts: { status: 'not_started' },
+ initialOrders: { status: 'not_started' },
+ };
+}
async function getActorUserId(data: AppData) {
const { currentUser } = await getCurrentUserContext(data);
return currentUser?.id;
}
+export async function updateOperationalOnboardingSectionAction(formData: FormData) {
+ const section = String(formData.get('section') ?? '') as OperationalOnboardingSectionKey;
+ const status = String(formData.get('status') ?? '') as OnboardingSectionStatus;
+
+ if (!operationalOnboardingSections.includes(section) || !operationalOnboardingStatuses.includes(status)) {
+ redirect('/admin/setup?section=onboarding&error=' + encodeURIComponent('Choose a valid onboarding section status.'));
+ }
+
+ const now = new Date().toISOString();
+ await persistence.write(({ instance }) => {
+ const current = instance.getInstanceState();
+ instance.updateInstanceState({
+ ...current,
+ operationalOnboarding: {
+ ...buildFallbackOperationalOnboarding(),
+ ...(current.operationalOnboarding ?? {}),
+ [section]: { status, updatedAt: now },
+ },
+ });
+ });
+
+ revalidateAllWorkspaces();
+ redirect('/admin/setup?section=onboarding&saved=onboarding-section');
+}
+
function revalidateAllWorkspaces() {
revalidatePath('/');
revalidatePath('/timeline');
diff --git a/lib/server/actions/entry-bootstrap.ts b/lib/server/actions/entry-bootstrap.ts
index 1ab54f8..63a7b2a 100644
--- a/lib/server/actions/entry-bootstrap.ts
+++ b/lib/server/actions/entry-bootstrap.ts
@@ -9,8 +9,8 @@ import { hashPassword, verifyPassword } from '@/lib/server/passwords';
import { isSupportedLocale, isSupportedPreset, localeCookieName, supportedLocales, presetCookieName, supportedPresets } from '@/lib/i18n/config';
import { themeCookieName } from '@/lib/theme';
import { getDefaultWorkspaceForRole, isPrimaryWorkspaceSurface } from '@/lib/workspaces';
-import { isNonProductionMode } from '@/lib/server/runtime-mode';
-import type { AppData } from '@/lib/domain/types';
+import { getRuntimeMode, isNonProductionMode } from '@/lib/server/runtime-mode';
+import type { AppData, EnvironmentType, OperationalOnboardingSections } from '@/lib/domain/types';
import { detectInstanceGatewayState, shouldRouteToEntryGateway } from '@/lib/server/instance-entry';
const supportedThemes = ['light', 'dark', 'system'] as const;
@@ -48,7 +48,7 @@ function nextBootstrapStep(current: number, direction: 'next' | 'back' | 'stay')
return current;
}
- return Math.min(8, current + 1);
+ return Math.min(3, current + 1);
}
function isRestorableBackupShape(value: unknown): value is AppData {
@@ -65,6 +65,49 @@ function isRestorableBackupShape(value: unknown): value is AppData {
);
}
+function getActivationEnvironmentType(): EnvironmentType {
+ const runtimeMode = getRuntimeMode();
+ if (runtimeMode === 'production') {
+ return 'production';
+ }
+ if (runtimeMode === 'pilot') {
+ return 'pilot';
+ }
+ return 'dev';
+}
+
+function buildInitialOperationalOnboarding(now: string): OperationalOnboardingSections {
+ return {
+ business: { status: 'ready', updatedAt: now },
+ team: { status: 'not_started' },
+ customers: { status: 'not_started' },
+ products: { status: 'not_started' },
+ recipes: { status: 'not_started' },
+ delivery: { status: 'not_started' },
+ shifts: { status: 'not_started' },
+ initialOrders: { status: 'not_started' },
+ };
+}
+
+function clearDemoOperationalRecordsForRealInstance(data: AppData) {
+ const ownerAdmin = data.users.find((user) => user.active && (user.role === 'owner_admin' || user.roles?.includes('owner_admin')))
+ ?? data.users.find((user) => user.role === 'owner_admin' || user.roles?.includes('owner_admin'));
+
+ data.users = ownerAdmin ? [ownerAdmin] : [];
+ data.customers = [];
+ data.destinations = [];
+ data.products = [];
+ data.suppliers = [];
+ data.rawMaterials = [];
+ data.supplierPriceEntries = [];
+ data.recipes = [];
+ data.recurringTemplates = [];
+ data.orders = [];
+ data.wipEntries = [];
+ data.shiftLogs = [];
+ data.activities = [];
+}
+
export async function resetLocalRuntimeDataAction() {
if (!isNonProductionMode()) {
redirect('/entry?error=' + encodeURIComponent('Local runtime reset is disabled in production mode.'));
@@ -362,6 +405,11 @@ export async function saveBootstrapStepAction(formData: FormData) {
const preset = String(formData.get('preset') ?? '').trim();
const operatingMode = String(formData.get('operatingMode') ?? '').trim();
const theme = String(formData.get('theme') ?? '').trim();
+ const dataMode = String(formData.get('dataMode') ?? 'real_clean').trim();
+
+ if (dataMode !== 'real_clean') {
+ redirect('/bootstrap?step=2&error=' + encodeURIComponent('Use the entry gateway for demo or restore. Real activation creates a clean instance.'));
+ }
if (!businessName) {
redirect('/bootstrap?step=2&error=' + encodeURIComponent('Business name is required.'));
@@ -379,95 +427,10 @@ export async function saveBootstrapStepAction(formData: FormData) {
}
data.instance.onboardingProgress.workspaceBasics = true;
data.instance.onboardingProgress.timezone = Boolean(data.workspace.timezone);
- }
-
- if (step === 4) {
- const now = new Date().toISOString();
- for (const row of Array.from({ length: 12 }, (_, index) => index + 1)) {
- const displayName = String(formData.get(`teamDisplayName${row}`) ?? '').trim();
- const username = slugifyUsername(String(formData.get(`teamUsername${row}`) ?? ''));
- const selectedRoles = formData.getAll(`teamRoles${row}`).map((value) => String(value));
- const defaultWorkspace = String(formData.get(`teamWorkspace${row}`) ?? 'orders');
- const enabled = formData.get(`teamEnabled${row}`) !== null;
- const roleList = selectedRoles
- .filter((role) => ['owner_admin', 'shift_lead', 'kitchen', 'sales'].includes(role))
- .filter((role, index, list) => list.indexOf(role) === index) as Array<'owner_admin' | 'shift_lead' | 'kitchen' | 'sales'>;
- const primaryRole = roleList[0] ?? 'sales';
-
- if (!displayName || !username) {
- continue;
- }
-
- const existing = data.users.find((user) => user.loginIdentifier.toLowerCase() === username);
- if (existing) {
- existing.displayName = displayName;
- existing.role = primaryRole;
- existing.roles = roleList.length > 0 ? roleList : [primaryRole];
- existing.defaultWorkspace = isPrimaryWorkspaceSurface(defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- ? (defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- : existing.defaultWorkspace;
- existing.active = enabled;
- existing.updatedAt = now;
- } else {
- data.users.push({
- id: `user-${crypto.randomUUID()}`,
- displayName,
- loginIdentifier: username,
- passwordHash: hashPassword('skoss-demo'),
- passwordUpdatedAt: now,
- mustChangePassword: true,
- role: primaryRole,
- roles: roleList.length > 0 ? roleList : [primaryRole],
- workspaceId: data.workspace.id,
- defaultWorkspace: isPrimaryWorkspaceSurface(defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- ? (defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- : 'orders',
- active: enabled,
- username,
- preferences: {
- defaultWorkspace: isPrimaryWorkspaceSurface(defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- ? (defaultWorkspace as 'home' | 'timeline' | 'orders' | 'customers' | 'production' | 'handoff' | 'preferences' | 'admin')
- : 'orders',
- },
- createdAt: now,
- updatedAt: now,
- });
- }
- }
- data.instance.onboardingProgress.users = true;
- data.instance.onboardingProgress.roles = true;
- }
-
- if (step === 5) {
- data.instance.onboardingProgress.shifts = true;
- }
-
- if (step === 6) {
- const productName = String(formData.get('starterProductName') ?? '').trim();
- const productUnit = String(formData.get('starterProductUnit') ?? '').trim() || 'pieces';
- if (productName) {
- const existing = data.products.find((product) => product.name.trim().toLowerCase() === productName.toLowerCase());
- if (!existing) {
- data.products.push({
- id: `product-${crypto.randomUUID()}`,
- name: productName,
- defaultUnit: productUnit,
- active: true,
- variants: [
- {
- id: `variant-${crypto.randomUUID()}`,
- name: 'Default',
- defaultUnit: productUnit,
- active: true,
- },
- ],
- });
- }
- }
- }
-
- if (step === 7) {
- data.instance.onboardingProgress.optionalImports = true;
+ data.instance.operationalOnboarding = {
+ ...(data.instance.operationalOnboarding ?? buildInitialOperationalOnboarding(new Date().toISOString())),
+ business: { status: 'ready', updatedAt: new Date().toISOString() },
+ };
}
if (intent === 'launch') {
@@ -478,11 +441,15 @@ export async function saveBootstrapStepAction(formData: FormData) {
redirect('/bootstrap?step=2&error=' + encodeURIComponent('Save workspace basics before launch.'));
}
+ const now = new Date().toISOString();
+ clearDemoOperationalRecordsForRealInstance(data);
data.preferences.onboardingCompleted = true;
- data.preferences.completedAt = data.preferences.completedAt ?? new Date().toISOString();
- data.preferences.updatedAt = new Date().toISOString();
+ data.preferences.completedAt = data.preferences.completedAt ?? now;
+ data.preferences.updatedAt = now;
data.instance.onboardingStatus = 'completed';
data.instance.demoModeActive = false;
+ data.instance.environmentType = getActivationEnvironmentType();
+ data.instance.operationalOnboarding = buildInitialOperationalOnboarding(now);
data.session.currentUserId = undefined;
await persistence.write(({ raw, instance, users }) => {
instance.updateInstanceState(data.instance);
@@ -490,27 +457,33 @@ export async function saveBootstrapStepAction(formData: FormData) {
instance.updatePreferences(data.preferences);
instance.updateSessionState(data.session);
users.replaceAll(data.users);
- if (step === 6) {
- raw.products = data.products;
- }
+ raw.customers = data.customers;
+ raw.destinations = data.destinations;
+ raw.products = data.products;
+ raw.suppliers = data.suppliers;
+ raw.rawMaterials = data.rawMaterials;
+ raw.supplierPriceEntries = data.supplierPriceEntries;
+ raw.recipes = data.recipes;
+ raw.recurringTemplates = data.recurringTemplates;
+ raw.orders = data.orders;
+ raw.wipEntries = data.wipEntries;
+ raw.shiftLogs = data.shiftLogs;
+ raw.activities = data.activities;
});
revalidateAllWorkspaces();
- redirect('/login?redirectTo=/');
+ redirect('/login?redirectTo=/admin/setup?section=onboarding');
}
- await persistence.write(({ raw, instance, users }) => {
+ await persistence.write(({ instance, users }) => {
instance.updateInstanceState(data.instance);
instance.updateWorkspace(data.workspace);
instance.updatePreferences(data.preferences);
instance.updateSessionState(data.session);
users.replaceAll(data.users);
- if (step === 6) {
- raw.products = data.products;
- }
});
revalidateAllWorkspaces();
if (intent === 'skip') {
- redirect(`/bootstrap?step=${Math.min(8, step + 1)}&saved=progress`);
+ redirect(`/bootstrap?step=${Math.min(3, step + 1)}&saved=progress`);
}
const nextStep = nextBootstrapStep(step, intent === 'back' ? 'back' : intent === 'stay' ? 'stay' : 'next');
@@ -612,7 +585,7 @@ export async function loginAction(formData: FormData) {
if (gatewayState.onboardingIncomplete && (user.roles?.includes('owner_admin') || user.roles?.includes('shift_lead') || user.role === 'owner_admin' || user.role === 'shift_lead')) {
revalidateAllWorkspaces();
- redirect('/bootstrap?step=1');
+ redirect('/admin/setup?section=onboarding');
}
revalidateAllWorkspaces();
diff --git a/lib/server/store.ts b/lib/server/store.ts
index 60efb8a..e72b640 100644
--- a/lib/server/store.ts
+++ b/lib/server/store.ts
@@ -9,6 +9,7 @@ import type {
Order,
OrderLine,
OnboardingProgress,
+ OperationalOnboardingSections,
Recipe,
RecurringTemplate,
User,
@@ -397,6 +398,17 @@ function hydrateStore(rawData: AppData): AppData {
const users = (rawData.users ?? []).map((user) => normalizeUser(user as Partial & { email?: string; role?: string }, rawData.workspace.id));
const hasAdminUser = users.some((user) => user.active && (user.roles?.includes('owner_admin') || user.role === 'owner_admin'));
const onboardingCompleted = rawData.preferences?.onboardingCompleted ?? false;
+ const operationalOnboarding: OperationalOnboardingSections = {
+ business: { status: rawData.instance?.onboardingProgress?.workspaceBasics ? 'ready' : 'not_started' },
+ team: { status: users.filter((user) => user.active).length > 1 ? 'ready' : 'not_started' },
+ customers: { status: (rawData.customers ?? []).length > 0 ? 'ready' : 'not_started' },
+ products: { status: (rawData.products ?? []).length > 0 ? 'ready' : 'not_started' },
+ recipes: { status: (rawData.recipes ?? []).length > 0 ? 'ready' : 'not_started' },
+ delivery: { status: (rawData.destinations ?? []).length > 0 ? 'ready' : 'not_started' },
+ shifts: { status: rawData.instance?.onboardingProgress?.shifts || (rawData.shiftLogs ?? []).length > 0 ? 'ready' : 'not_started' },
+ initialOrders: { status: (rawData.orders ?? []).length > 0 ? 'ready' : 'not_started' },
+ ...(rawData.instance?.operationalOnboarding ?? {}),
+ };
const instance: InstanceState = {
initialized: rawData.instance?.initialized ?? users.length > 0,
onboardingStatus: rawData.instance?.onboardingStatus
@@ -414,6 +426,7 @@ function hydrateStore(rawData: AppData): AppData {
users: rawData.instance?.onboardingProgress?.users ?? users.length > 0,
roles: rawData.instance?.onboardingProgress?.roles ?? hasAdminUser,
},
+ operationalOnboarding,
operatorOnboardingByUserId: rawData.instance?.operatorOnboardingByUserId ?? {},
moduleStates: getModuleStateMap(preferences.preset, rawData.instance?.moduleStates),
};