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
50 changes: 10 additions & 40 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 { skossBootstrapRoutes, skossCoreRoutes } from '@/lib/application-planes';
import { skossCoreRoutes } from '@/lib/application-planes';
import {
formatCurrency,
formatDateLabel,
Expand All @@ -26,6 +26,7 @@ import { getSetupWorkspace } from '@/lib/server/demo-data';
import { getServerTranslator } from '@/lib/i18n/server';
import { getVisibleWorkspacesForRole } from '@/lib/workspaces';
import { ThemeSwitcher } from '@/components/theme-switcher';
import { OperationalOnboardingChecklist } from '@/components/setup/operational-onboarding-checklist';
import { getRuntimeMode, isNonProductionMode } from '@/lib/server/runtime-mode';

const basicUnits = [
Expand All @@ -41,6 +42,7 @@ const basicUnits = [

type SetupSearchParams = {
section?:
| 'onboarding'
| 'business-setup'
| 'users'
| 'preferences-system'
Expand Down Expand Up @@ -168,6 +170,10 @@ async function SavedMessage({ saved }: { saved?: string }) {
return <p className="inline-success">Demo workspace reseeded successfully.</p>;
}

if (saved === 'onboarding-section') {
return <p className="inline-success">Onboarding section updated.</p>;
}

return null;
}

Expand Down Expand Up @@ -344,6 +350,7 @@ export default async function SetupPage({
const setupSection = params?.section;
const sectionNavItems: Array<{
key:
| 'onboarding'
| 'business-setup'
| 'users'
| 'preferences-system'
Expand All @@ -355,6 +362,7 @@ export default async function SetupPage({
| 'price-history';
label: string;
}> = [
{ key: 'onboarding', label: 'Onboarding' },
{ key: 'business-setup', label: t('setup.sections.businessSetup') },
{ key: 'users', label: t('setup.sections.users') },
{ key: 'preferences-system', label: t('setup.sections.preferencesSystem') },
Expand All @@ -370,14 +378,6 @@ export default async function SetupPage({
query: { section },
hash: section,
});
const onboardingProgress = identitySetup.instance.onboardingProgress;
const guidedChecklist = [
{ label: 'Admin account', done: onboardingProgress.adminAccount },
{ label: 'Workspace basics', done: onboardingProgress.workspaceBasics },
{ label: 'Team setup', done: onboardingProgress.users },
{ label: 'Operational rhythm', done: onboardingProgress.shifts },
{ label: 'Optional imports', done: onboardingProgress.optionalImports },
];

return (
<div className="page-stack">
Expand Down Expand Up @@ -445,37 +445,7 @@ export default async function SetupPage({
</div>
</section>

<section className="panel page-stack">
<div className="table-header-row">
<div>
<h2>Setup checklist (resumable)</h2>
<p className="helper-text">
Keep activation practical: only the core is required at launch, everything else can be completed gradually.
</p>
</div>
<span className="summary-pill">
{guidedChecklist.filter((item) => item.done).length}/{guidedChecklist.length}
</span>
</div>
<ul className="stack-list compact-list">
{guidedChecklist.map((item) => (
<li key={item.label} className="list-with-actions">
<div>
<strong>{item.label}</strong>
<span>{item.done ? 'Completed' : 'Pending'}</span>
</div>
</li>
))}
</ul>
<div className="inline-action-row">
<Link href={skossBootstrapRoutes.bootstrapStart} className="button-secondary compact-button">
Resume guided activation
</Link>
<a href="#suppliers" className="button-secondary compact-button">
Continue CSV imports
</a>
</div>
</section>
<OperationalOnboardingChecklist sections={identitySetup.instance.operationalOnboarding} />

<section className="grid-two" id="business-setup">
<article className="panel page-stack" id="customers-summary">
Expand Down
193 changes: 48 additions & 145 deletions app/bootstrap/page.tsx
Original file line number Diff line number Diff line change
@@ -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));
Expand Down Expand Up @@ -35,19 +33,19 @@ export default async function BootstrapPage({
<section className="hero-card">
<div className="hero-header">
<div>
<p className="eyebrow">First-time setup</p>
<h1>Start your kitchen workspace</h1>
<p className="eyebrow">Required activation</p>
<h1>Create a clean SKOSS instance</h1>
<p className="lede">
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.
</p>
</div>
<span className="summary-pill">Step {step} of {totalSteps}</span>
</div>
<div className="bootstrap-progress-track">
<span className="bootstrap-progress-value" style={{ width: `${progress}%` }} aria-hidden="true" />
<span className="bootstrap-progress-value" style={{ width: progress + '%' }} aria-hidden="true" />
</div>
<p className="helper-text no-margin">
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.
</p>
</section>

Expand All @@ -59,7 +57,8 @@ export default async function BootstrapPage({

{step === 1 ? (
<section className="page-stack">
<h2>Admin account (required)</h2>
<h2>Owner/admin account</h2>
<p className="helper-text">This is the first active user kept when the clean instance is launched.</p>
<div className="grid-two">
<label>
<span className="field-heading">Full name <span className="setup-required-mark" aria-hidden="true">*</span></span>
Expand All @@ -74,11 +73,11 @@ export default async function BootstrapPage({
<div className="grid-two">
<label>
<span className="field-heading">Email (optional)</span>
<input name="adminEmail" type="email" placeholder="owner@kitchen.local" />
<input name="adminEmail" type="email" defaultValue={adminUser?.email ?? ''} placeholder="owner@kitchen.local" />
</label>
<label>
<span className="field-heading">Phone (optional)</span>
<input name="adminPhone" placeholder="+1 555 0100" />
<input name="adminPhone" defaultValue={adminUser?.phone ?? ''} placeholder="+1 555 0100" />
</label>
</div>
<label>
Expand All @@ -90,7 +89,7 @@ export default async function BootstrapPage({

{step === 2 ? (
<section className="page-stack">
<h2>Workspace basics (required)</h2>
<h2>Business identity and data mode</h2>
<div className="grid-two">
<label>
<span className="field-heading">Business name <span className="setup-required-mark" aria-hidden="true">*</span></span>
Expand All @@ -108,7 +107,7 @@ export default async function BootstrapPage({
</select>
</label>
<label>
<span className="field-heading">Theme</span>
<span className="field-heading">Appearance</span>
<select name="theme" defaultValue={data.preferences.theme}>
<option value="light">Light</option>
<option value="dark">Dark</option>
Expand All @@ -117,155 +116,59 @@ export default async function BootstrapPage({
</label>
</div>
<fieldset className="subpanel page-stack">
<legend className="field-heading">Operating mode</legend>
<legend className="field-heading">Instance/data mode</legend>
<label className="checkbox-row">
<input type="radio" name="dataMode" value="real_clean" defaultChecked />
<span>
<strong>Clean real instance</strong>
<span className="helper-text">Launch with no demo customers, products, orders, recurring templates, WIP, activities, destinations, suppliers, or demo team members.</span>
</span>
</label>
<p className="helper-text no-margin">Use the entry gateway instead for demo mode or restore from backup.</p>
</fieldset>
<fieldset className="subpanel page-stack">
<legend className="field-heading">Business preset or operational profile</legend>
<div className="grid-two">
<label className="checkbox-row"><input type="radio" name="operatingMode" value="pickup" defaultChecked={data.preferences.operatingMode === 'pickup'} /><span><strong>Pickup</strong><span className="helper-text">Most orders are picked up at your location.</span></span></label>
<label className="checkbox-row"><input type="radio" name="operatingMode" value="delivery" defaultChecked={data.preferences.operatingMode === 'delivery'} /><span><strong>Delivery</strong><span className="helper-text">Most orders go out for delivery.</span></span></label>
<label className="checkbox-row"><input type="radio" name="operatingMode" value="mixed" defaultChecked={data.preferences.operatingMode === 'mixed'} /><span><strong>Mixed</strong><span className="helper-text">You handle both pickup and delivery.</span></span></label>
</div>
</fieldset>
<label>
<span className="field-heading">Preset</span>
<select name="preset" defaultValue={data.preferences.preset}>
<option value="bakery">Bakery — dough and bake oriented</option>
<option value="cafe">Cafe — service and prep oriented</option>
<option value="restaurant">Restaurant — shift and order paced</option>
<option value="dark_kitchen">Dark kitchen — delivery-first flow</option>
<option value="food_stall">Food stall — fast daily rhythm</option>
<option value="generic">Generic — flexible baseline</option>
<option value="other">Other — start simple</option>
</select>
</label>
<p className="helper-text">You can change all of this later.</p>
</section>
) : null}

{step === 3 ? (
<section className="page-stack">
<h2>Work style (optional)</h2>
<p>Choose how you run day-to-day. This helps us suggest defaults.</p>
<div className="grid-two">
<label className="checkbox-row"><input type="radio" name="workStyle" value="solo" defaultChecked /><span><strong>Solo operator</strong><span className="helper-text">One person handles most tasks.</span></span></label>
<label className="checkbox-row"><input type="radio" name="workStyle" value="team" /><span><strong>Small team</strong><span className="helper-text">You hand work across roles and shifts.</span></span></label>
</div>
</section>
) : null}

{step === 4 ? (
<section className="page-stack">
<h2>Team and roles (optional)</h2>
<p>Add one user or a full small team. You can also skip and continue from Setup later.</p>
<TeamRosterBuilder initialRows={1} />
<p className="helper-text">
Team members created here receive a temporary password and must update it at first sign-in.
</p>
</section>
) : null}

{step === 5 ? (
<section className="page-stack">
<h2>Operational rhythm (optional)</h2>
<p>Set your basic working hours. Keep this light; detailed scheduling can come later.</p>
<div className="grid-two">
<label>
<span className="field-heading">Start time</span>
<input name="rhythmStart" type="time" defaultValue="06:00" />
</label>
<label>
<span className="field-heading">End time</span>
<input name="rhythmEnd" type="time" defaultValue="18:00" />
</label>
</div>
</section>
) : null}

{step === 6 ? (
<section className="page-stack">
<h2>First product (optional)</h2>
<p>Create one starter product now for immediate order capture, or skip and add products later.</p>
<div className="grid-two">
<label>
<span className="field-heading">Product name</span>
<input name="starterProductName" placeholder="Sourdough loaf" />
</label>
<label>
<span className="field-heading">Unit</span>
<input name="starterProductUnit" placeholder="pieces" />
<span className="field-heading">Preset</span>
<select name="preset" defaultValue={data.preferences.preset}>
<option value="bakery">Bakery - dough and bake oriented</option>
<option value="cafe">Cafe - service and prep oriented</option>
<option value="restaurant">Restaurant - shift and order paced</option>
<option value="dark_kitchen">Dark kitchen - delivery-first flow</option>
<option value="food_stall">Food stall - fast daily rhythm</option>
<option value="generic">Generic - flexible baseline</option>
<option value="other">Other - start simple</option>
</select>
</label>
</div>
<p className="helper-text">Need bulk import later? Use Setup → Imports after launch.</p>
</section>
) : null}

{step === 7 ? (
<section className="page-stack">
<h2>Imports (optional)</h2>
<p>Import customers, suppliers, or materials if you already have CSV files. Manual setup remains available in Setup.</p>
<div className="grid-two">
<CsvImportCard
entity="customers"
title="Customer import"
description="Bring repeating customer records into the instance."
hint="CSV sample"
sampleHeader="displayName,phone,email,address,deliveryNote,internalNote"
actionLabel="Import customers"
chooseFileLabel="Choose CSV"
mappingTitle="Map columns"
previewTitle="Preview"
emptyPreview="No rows"
noFileLabel="No file selected"
parseErrorNoColumns="Could not detect CSV columns."
parseErrorReadFile="Could not read CSV file."
fields={[{ key: 'displayName', label: 'Customer', required: true }, { key: 'phone', label: 'Phone' }, { key: 'email', label: 'Email' }]}
action={importCsvEntitiesAction}
redirectTo="/bootstrap?step=7"
/>
<CsvImportCard
entity="suppliers"
title="Supplier import"
description="Add supplier records from your existing file."
hint="CSV sample"
sampleHeader="name,contact,notes"
actionLabel="Import suppliers"
chooseFileLabel="Choose CSV"
mappingTitle="Map columns"
previewTitle="Preview"
emptyPreview="No rows"
noFileLabel="No file selected"
parseErrorNoColumns="Could not detect CSV columns."
parseErrorReadFile="Could not read CSV file."
fields={[{ key: 'name', label: 'Supplier', required: true }, { key: 'contact', label: 'Contact' }, { key: 'notes', label: 'Notes' }]}
action={importCsvEntitiesAction}
redirectTo="/bootstrap?step=7"
/>
</div>
</fieldset>
</section>
) : null}

{step === 8 ? (
{step === 3 ? (
<section className="page-stack">
<h2>Review and launch</h2>
<ul className="stack-list">
<li><strong>Identity:</strong> {data.instance.onboardingProgress.adminAccount ? 'ready' : 'missing'}</li>
<li><strong>Kitchen basics:</strong> {data.instance.onboardingProgress.workspaceBasics ? 'saved' : 'pending'}</li>
<li><strong>Team:</strong> {data.instance.onboardingProgress.users ? 'saved' : 'pending'}</li>
<li><strong>Operational rhythm:</strong> {data.instance.onboardingProgress.shifts ? 'saved' : 'pending'}</li>
<li><strong>Imports:</strong> optional {data.instance.onboardingProgress.optionalImports ? 'attempted' : 'skipped'}</li>
<h2>Confirm activation</h2>
<ul className="stack-list compact-list">
<li><strong>Owner/admin:</strong> {data.instance.onboardingProgress.adminAccount ? 'ready' : 'missing'}</li>
<li><strong>Business basics:</strong> {data.instance.onboardingProgress.workspaceBasics ? 'ready' : 'missing'}</li>
<li><strong>Data mode:</strong> clean real instance</li>
<li><strong>Next:</strong> sign in and continue the operational onboarding checklist.</li>
</ul>
<p className="helper-text">
Launch takes you to sign-in. Optional items can be resumed later from Setup.
Launch will clear demo operational records and keep only the owner/admin account plus structural instance settings.
</p>
</section>
) : null}

<div className="onboarding-actions">
{step > 1 ? <button type="submit" name="intent" value="back" className="button-secondary">Back</button> : null}
{step < 8 ? (
<>
{!isRequiredStep ? <button type="submit" name="intent" value="skip" className="button-ghost">Skip for now</button> : null}
<button type="submit" name="intent" value="next" className="button-primary">Next</button>
</>
) : <button type="submit" name="intent" value="launch" className="button-primary">Launch workspace</button>}
{step < totalSteps ? (
<button type="submit" name="intent" value="next" className="button-primary">Next</button>
) : <button type="submit" name="intent" value="launch" className="button-primary" disabled={!isRequiredStep}>Create instance</button>}
</div>
</form>
</div>
Expand Down
Loading
Loading