guidence fix
This commit is contained in:
@@ -1,110 +1,184 @@
|
|||||||
/**
|
/**
|
||||||
* Getting a new shop from "signed in" to "on sale", on the page they land on.
|
* Walking a new merchant from "signed in" to "on sale", one step at a time.
|
||||||
*
|
*
|
||||||
* There was no onboarding of any kind in this console — no checklist, no tour,
|
* It began as a plain checklist and that was not enough: seven lines all
|
||||||
* no first-run anything. A brand-new merchant landed on a dashboard showing
|
* demanding attention equally is a list of homework, not guidance. Somebody who
|
||||||
* four KPI cards reading ₹0 and a line saying "No branches yet" with nothing to
|
* has never used the console does not need to be told there are seven things —
|
||||||
* click, which reads as *everything is fine* rather than *nothing is set up*.
|
* they need to be told the ONE thing to do now, with a button that does it.
|
||||||
*
|
*
|
||||||
* Kmart is the argument for this screen: one branch, two products, both
|
* So one step is in front of you at a time, with a real action and a way past
|
||||||
* unpriced, so nothing had ever been sellable — stuck since July with no
|
* it. The rest are a progress rail: visible, so nobody feels tricked about how
|
||||||
* screen anywhere saying which step they were on.
|
* far there is to go, but quiet.
|
||||||
*
|
*
|
||||||
* Every tick is derived from live data, so the card cannot claim work that was
|
* Every tick is derived from live data (see `setupSteps.ts`), so the card can
|
||||||
* not done, and it disappears entirely once the shop is trading.
|
* never claim work that was not done. The only stored thing is what somebody
|
||||||
|
* chose to skip, and skipping never marks a step done — an unpriced catalogue
|
||||||
|
* does not start selling because a card was dismissed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Button } from '@astryxdesign/core/Button';
|
||||||
import { Card } from '@astryxdesign/core/Card';
|
import { Card } from '@astryxdesign/core/Card';
|
||||||
import { HStack } from '@astryxdesign/core/HStack';
|
import { HStack } from '@astryxdesign/core/HStack';
|
||||||
import { Text } from '@astryxdesign/core/Text';
|
import { Text } from '@astryxdesign/core/Text';
|
||||||
import { VStack } from '@astryxdesign/core/VStack';
|
import { VStack } from '@astryxdesign/core/VStack';
|
||||||
import { Link } from 'react-router-dom';
|
import { ArrowRight, Check } from 'lucide-react';
|
||||||
import { Check, Circle } from 'lucide-react';
|
import { setupSteps, isSetupComplete, type SetupInput, type SetupStep } from './setupSteps';
|
||||||
import { setupSteps, currentStep, isSetupComplete, type SetupInput } from './setupSteps';
|
import { focusStep, isPaused, resumeAll, skipStep, skippedSteps } from './setupProgress';
|
||||||
|
|
||||||
export function SetupChecklist(input: SetupInput) {
|
export function SetupChecklist(input: SetupInput & { tenantid: number }) {
|
||||||
const steps = setupSteps(input);
|
const { tenantid, ...data } = input;
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [skipped, setSkipped] = useState(() => skippedSteps(tenantid));
|
||||||
|
|
||||||
// Gone once the shop is trading. A checklist that lingers after it is
|
const steps = useMemo(() => setupSteps(data), [data]);
|
||||||
// finished becomes furniture, and stops being read the next time it matters.
|
const done = steps.filter((step) => step.done).length;
|
||||||
|
const focus = focusStep(steps, skipped);
|
||||||
|
const paused = isPaused(steps, skipped);
|
||||||
|
|
||||||
|
// Gone once the shop is selling. A card that outlives its purpose becomes
|
||||||
|
// furniture and stops being read the next time it matters.
|
||||||
if (isSetupComplete(steps)) return null;
|
if (isSetupComplete(steps)) return null;
|
||||||
|
|
||||||
const now = currentStep(steps);
|
/* Everything outstanding was waved past. One quiet line rather than nothing:
|
||||||
const done = steps.filter((step) => step.done).length;
|
the work is still undone, and a card that vanished entirely would leave no
|
||||||
|
way back to it. */
|
||||||
|
if (paused || !focus) {
|
||||||
|
return (
|
||||||
|
<Card padding={2} variant="transparent">
|
||||||
|
<HStack justify="between" align="center" gap={2} wrap="wrap">
|
||||||
|
<Text type="body" size="sm" color="secondary">
|
||||||
|
Setup paused — {done} of {steps.length} done.
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
label="Resume"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSkipped(resumeAll(tenantid))}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = steps.findIndex((step) => step.id === focus.id) + 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding={3} elevation="low">
|
<Card padding={3} elevation="low">
|
||||||
<VStack gap={2}>
|
<VStack gap={2}>
|
||||||
<VStack gap={0.5}>
|
|
||||||
<HStack justify="between" align="center" gap={2} wrap="wrap">
|
<HStack justify="between" align="center" gap={2} wrap="wrap">
|
||||||
<Text type="label" size="lg" weight="semibold">
|
<Text
|
||||||
|
type="label"
|
||||||
|
size="xsm"
|
||||||
|
color="secondary"
|
||||||
|
style={{ textTransform: 'uppercase', letterSpacing: '0.09em' }}
|
||||||
|
>
|
||||||
Set up your shop
|
Set up your shop
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text type="body" size="sm" color="secondary" hasTabularNumbers>
|
||||||
type="body"
|
Step {position} of {steps.length}
|
||||||
size="sm"
|
|
||||||
color="secondary"
|
|
||||||
hasTabularNumbers
|
|
||||||
>
|
|
||||||
{done} of {steps.length}
|
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
{/* The one sentence that was missing: which step you are on, and what
|
|
||||||
to do about it. */}
|
{/* The one thing to do now. Everything else on this card is context. */}
|
||||||
{now ? (
|
<VStack gap={1}>
|
||||||
<Text type="body" size="sm" color="secondary">
|
<Text type="large" weight="semibold">
|
||||||
{now.todo}
|
{focus.title}
|
||||||
|
</Text>
|
||||||
|
<Text type="body" size="sm" color="secondary" style={{ lineHeight: 1.6, maxWidth: '60ch' }}>
|
||||||
|
{focus.todo}
|
||||||
|
</Text>
|
||||||
|
{focus.detail ? (
|
||||||
|
<Text type="body" size="xsm" color="secondary">
|
||||||
|
{focus.detail}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
<VStack gap={0}>
|
<HStack gap={1.5} align="center" wrap="wrap">
|
||||||
{steps.map((step) => {
|
<Button
|
||||||
const isNow = step.id === now?.id;
|
label={focus.cta}
|
||||||
return (
|
variant="primary"
|
||||||
<Link
|
|
||||||
key={step.id}
|
|
||||||
to={step.href}
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 10,
|
|
||||||
padding: '9px 8px',
|
|
||||||
borderRadius: 8,
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
background: isNow ? 'var(--color-surface-subtle)' : 'transparent',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{step.done ? (
|
|
||||||
<Check size={15} style={{ color: 'var(--color-success, #10b981)', flex: 'none' }} />
|
|
||||||
) : (
|
|
||||||
<Circle
|
|
||||||
size={15}
|
|
||||||
style={{
|
|
||||||
color: isNow ? 'var(--color-brand)' : 'var(--color-ink-4)',
|
|
||||||
flex: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
type="body"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
{...(isNow ? { weight: 'semibold' as const } : {})}
|
endContent={<ArrowRight size={14} />}
|
||||||
{...(step.done ? { color: 'secondary' as const } : {})}
|
onClick={() => navigate(focus.href)}
|
||||||
>
|
/>
|
||||||
{step.title}
|
{/* Skipping is offered, not hidden. Somebody who cannot do this step
|
||||||
</Text>
|
today — no licence to hand, no staff hired yet — should be able to
|
||||||
{step.detail ? (
|
get on with the rest rather than abandon the guidance entirely.
|
||||||
<Text type="body" size="xsm" color="secondary" hasTabularNumbers>
|
The step stays open in the rail. */}
|
||||||
{step.detail}
|
<Button
|
||||||
</Text>
|
label="Skip for now"
|
||||||
) : null}
|
variant="ghost"
|
||||||
</Link>
|
size="sm"
|
||||||
);
|
onClick={() => setSkipped(skipStep(tenantid, focus.id))}
|
||||||
})}
|
/>
|
||||||
</VStack>
|
</HStack>
|
||||||
|
|
||||||
|
<ProgressRail steps={steps} focusId={focus.id} skipped={skipped} />
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far there is to go, without making it the point of the card.
|
||||||
|
*
|
||||||
|
* Names rather than bare pips: "step 4 of 7" tells somebody how much is left
|
||||||
|
* but not what is coming, and a merchant deciding whether to start now wants
|
||||||
|
* both. Muted enough that the focused step above still reads first.
|
||||||
|
*/
|
||||||
|
function ProgressRail({
|
||||||
|
steps,
|
||||||
|
focusId,
|
||||||
|
skipped,
|
||||||
|
}: {
|
||||||
|
steps: readonly SetupStep[];
|
||||||
|
focusId: string;
|
||||||
|
skipped: readonly string[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<VStack gap={0} style={{ borderTop: '1px solid var(--color-line)', paddingTop: 12 }}>
|
||||||
|
{steps.map((step) => {
|
||||||
|
const isFocus = step.id === focusId;
|
||||||
|
const isSkipped = !step.done && skipped.includes(step.id);
|
||||||
|
return (
|
||||||
|
<HStack key={step.id} gap={1} align="center" style={{ padding: '4px 0' }}>
|
||||||
|
{step.done ? (
|
||||||
|
<Check size={13} style={{ color: 'var(--color-success, #10b981)', flex: 'none' }} />
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
style={{
|
||||||
|
width: 13,
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
flex: 'none',
|
||||||
|
color: isFocus ? 'var(--color-brand)' : 'var(--color-ink-4)',
|
||||||
|
fontSize: 11,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
●
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
type="body"
|
||||||
|
size="xsm"
|
||||||
|
{...(step.done || !isFocus ? { color: 'secondary' as const } : {})}
|
||||||
|
{...(isFocus ? { weight: 'semibold' as const } : {})}
|
||||||
|
style={isSkipped ? { opacity: 0.55 } : undefined}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</Text>
|
||||||
|
{isSkipped ? (
|
||||||
|
<Text type="body" size="xsm" color="secondary">
|
||||||
|
skipped
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ export function ConsolePage() {
|
|||||||
of things they must ask somebody else to do. */}
|
of things they must ask somebody else to do. */}
|
||||||
{!isPinned ? (
|
{!isPinned ? (
|
||||||
<SetupChecklist
|
<SetupChecklist
|
||||||
|
tenantid={tenantid}
|
||||||
shop={shop.data}
|
shop={shop.data}
|
||||||
people={people.data ?? []}
|
people={people.data ?? []}
|
||||||
branches={branches}
|
branches={branches}
|
||||||
|
|||||||
77
src/features/store-admin/setupProgress.test.ts
Normal file
77
src/features/store-admin/setupProgress.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Which step a merchant is walked through, and what skipping does.
|
||||||
|
*
|
||||||
|
* The rule that matters: skipping moves the guidance on, it never marks work
|
||||||
|
* done. A shop whose catalogue is unpriced does not start selling because
|
||||||
|
* somebody pressed "Skip for now", and a card that implied otherwise would be
|
||||||
|
* worse than no card.
|
||||||
|
*/
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { test, beforeEach } from 'node:test';
|
||||||
|
import { focusStep, isPaused } from './setupProgress';
|
||||||
|
import type { SetupStep, SetupStepId } from './setupSteps';
|
||||||
|
|
||||||
|
const step = (id: SetupStepId, done: boolean): SetupStep => ({
|
||||||
|
id,
|
||||||
|
title: id,
|
||||||
|
todo: '',
|
||||||
|
cta: '',
|
||||||
|
done,
|
||||||
|
href: '/',
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
step('profile', false),
|
||||||
|
step('people', false),
|
||||||
|
step('branch', true),
|
||||||
|
step('products', false),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// `localStorage` does not exist under the test runner, and the module must
|
||||||
|
// survive that — a private window and a browser refusing site data reach the
|
||||||
|
// same code path.
|
||||||
|
delete (globalThis as { localStorage?: unknown }).localStorage;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the focus is the first step that is neither done nor skipped', () => {
|
||||||
|
assert.equal(focusStep(steps, [])?.id, 'profile');
|
||||||
|
assert.equal(focusStep(steps, ['profile'])?.id, 'people');
|
||||||
|
assert.equal(focusStep(steps, ['profile', 'people'])?.id, 'products');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Done steps are stepped over whether or not they were skipped — 'branch' is
|
||||||
|
// already complete and never becomes the focus.
|
||||||
|
test('a completed step is never focused', () => {
|
||||||
|
assert.notEqual(focusStep(steps, ['profile', 'people'])?.id, 'branch');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skipping everything outstanding leaves nothing to focus', () => {
|
||||||
|
assert.equal(focusStep(steps, ['profile', 'people', 'products']), null);
|
||||||
|
assert.equal(isPaused(steps, ['profile', 'people', 'products']), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Paused is not finished. Every outstanding step being skipped means the merchant
|
||||||
|
waved the guidance past — the work is still undone, which is why the card shows
|
||||||
|
a resume line rather than disappearing.
|
||||||
|
*/
|
||||||
|
test('paused is false while any outstanding step is unskipped', () => {
|
||||||
|
assert.equal(isPaused(steps, ['profile']), false);
|
||||||
|
assert.equal(isPaused(steps, []), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// A shop with nothing left to do is not "paused" either — there is simply
|
||||||
|
// nothing outstanding to have skipped.
|
||||||
|
test('a finished shop is not reported as paused', () => {
|
||||||
|
const finished = [step('profile', true), step('people', true)];
|
||||||
|
assert.equal(isPaused(finished, []), false);
|
||||||
|
assert.equal(isPaused(finished, ['profile']), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Storage the browser refuses must not take the page down, and must fail
|
||||||
|
// towards showing the guidance rather than silently hiding it.
|
||||||
|
test('unavailable storage reads as nothing skipped', async () => {
|
||||||
|
const { skippedSteps } = await import('./setupProgress');
|
||||||
|
assert.deepEqual(skippedSteps(1141), []);
|
||||||
|
});
|
||||||
84
src/features/store-admin/setupProgress.ts
Normal file
84
src/features/store-admin/setupProgress.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { SetupStep, SetupStepId } from './setupSteps';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which step a merchant is being walked through, and which they have waved
|
||||||
|
* past.
|
||||||
|
*
|
||||||
|
* Skipping is a preference, not a fact, so it is the one thing here that IS
|
||||||
|
* stored rather than derived — in `localStorage`, per browser, per shop. It
|
||||||
|
* deliberately does not travel: "not now" is a statement about this afternoon,
|
||||||
|
* not a decision about the business, and a colleague opening the console should
|
||||||
|
* still be shown what is outstanding.
|
||||||
|
*
|
||||||
|
* A skipped step is never marked done. It drops out of the guided sequence and
|
||||||
|
* stays visibly incomplete in the list, because the work still has to happen —
|
||||||
|
* an unpriced catalogue does not start selling because somebody dismissed a
|
||||||
|
* card.
|
||||||
|
*/
|
||||||
|
const KEY = 'nearle.setup.skipped';
|
||||||
|
|
||||||
|
type SkipMap = Record<string, SetupStepId[]>;
|
||||||
|
|
||||||
|
function read(): SkipMap {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(KEY);
|
||||||
|
const parsed: unknown = raw ? JSON.parse(raw) : {};
|
||||||
|
return parsed && typeof parsed === 'object' ? (parsed as SkipMap) : {};
|
||||||
|
} catch {
|
||||||
|
// A private window, cleared site data, or storage the browser refuses.
|
||||||
|
// Nothing skipped is the safe answer: the merchant sees the guidance again
|
||||||
|
// rather than losing it silently.
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skippedSteps(tenantid: number): SetupStepId[] {
|
||||||
|
return read()[String(tenantid)] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function skipStep(tenantid: number, id: SetupStepId): SetupStepId[] {
|
||||||
|
const all = read();
|
||||||
|
const key = String(tenantid);
|
||||||
|
const next = [...new Set([...(all[key] ?? []), id])];
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, JSON.stringify({ ...all, [key]: next }));
|
||||||
|
} catch {
|
||||||
|
/* Storage refused. The skip applies to this render either way — losing it
|
||||||
|
on reload is a smaller failure than the write throwing mid-click. */
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resumeAll(tenantid: number): SetupStepId[] {
|
||||||
|
const all = read();
|
||||||
|
try {
|
||||||
|
localStorage.setItem(KEY, JSON.stringify({ ...all, [String(tenantid)]: [] }));
|
||||||
|
} catch {
|
||||||
|
/* As above. */
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one step to put in front of somebody: the first that is neither done nor
|
||||||
|
* skipped.
|
||||||
|
*
|
||||||
|
* Null when there is nothing left to guide — either everything is done, or
|
||||||
|
* everything outstanding has been waved past. The two are different states and
|
||||||
|
* the card renders them differently; this only says there is no step to focus.
|
||||||
|
*/
|
||||||
|
export function focusStep(
|
||||||
|
steps: readonly SetupStep[],
|
||||||
|
skipped: readonly SetupStepId[],
|
||||||
|
): SetupStep | null {
|
||||||
|
return steps.find((step) => !step.done && !skipped.includes(step.id)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when work remains but every outstanding step has been waved past. */
|
||||||
|
export function isPaused(
|
||||||
|
steps: readonly SetupStep[],
|
||||||
|
skipped: readonly SetupStepId[],
|
||||||
|
): boolean {
|
||||||
|
const outstanding = steps.filter((step) => !step.done);
|
||||||
|
return outstanding.length > 0 && outstanding.every((step) => skipped.includes(step.id));
|
||||||
|
}
|
||||||
@@ -36,6 +36,14 @@ export interface SetupStep {
|
|||||||
title: string;
|
title: string;
|
||||||
/** What to do, when it is not done. Never shown once it is. */
|
/** What to do, when it is not done. Never shown once it is. */
|
||||||
todo: string;
|
todo: string;
|
||||||
|
/**
|
||||||
|
* The button that starts the work.
|
||||||
|
*
|
||||||
|
* Named for the action, not the destination — "Add your shop details" rather
|
||||||
|
* than "Go to profile". A step somebody is being walked through has to say
|
||||||
|
* what pressing it does, or it reads as navigation and gets ignored.
|
||||||
|
*/
|
||||||
|
cta: string;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
/** Where the work happens. */
|
/** Where the work happens. */
|
||||||
href: string;
|
href: string;
|
||||||
@@ -66,6 +74,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'profile',
|
id: 'profile',
|
||||||
title: 'Complete your shop profile',
|
title: 'Complete your shop profile',
|
||||||
todo: 'Add your shop photo and licence — this is what shoppers see.',
|
todo: 'Add your shop photo and licence — this is what shoppers see.',
|
||||||
|
cta: 'Add your shop details',
|
||||||
done: isProfileComplete(shop ?? {}),
|
done: isProfileComplete(shop ?? {}),
|
||||||
href: '/admin/profile',
|
href: '/admin/profile',
|
||||||
},
|
},
|
||||||
@@ -73,6 +82,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'people',
|
id: 'people',
|
||||||
title: 'Add your people',
|
title: 'Add your people',
|
||||||
todo: 'Add whoever will run your shops. You can add them before a branch exists.',
|
todo: 'Add whoever will run your shops. You can add them before a branch exists.',
|
||||||
|
cta: 'Add a person',
|
||||||
done: people.length > 0,
|
done: people.length > 0,
|
||||||
href: '/admin/users',
|
href: '/admin/users',
|
||||||
...(people.some(isUnplaced)
|
...(people.some(isUnplaced)
|
||||||
@@ -83,6 +93,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'branch',
|
id: 'branch',
|
||||||
title: 'Open your first branch',
|
title: 'Open your first branch',
|
||||||
todo: 'Commission an outlet and say who runs it.',
|
todo: 'Commission an outlet and say who runs it.',
|
||||||
|
cta: 'Open a branch',
|
||||||
done: branches.length > 0,
|
done: branches.length > 0,
|
||||||
href: '/admin/branches/new',
|
href: '/admin/branches/new',
|
||||||
...(branches.length > 0 ? { detail: `${branches.length}` } : {}),
|
...(branches.length > 0 ? { detail: `${branches.length}` } : {}),
|
||||||
@@ -91,6 +102,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'products',
|
id: 'products',
|
||||||
title: 'Get your products in',
|
title: 'Get your products in',
|
||||||
todo: 'Import from the catalogue, or upload your own spreadsheet.',
|
todo: 'Import from the catalogue, or upload your own spreadsheet.',
|
||||||
|
cta: 'Add products',
|
||||||
done: products.length > 0,
|
done: products.length > 0,
|
||||||
href: '/admin/inventory',
|
href: '/admin/inventory',
|
||||||
/* The waiting state, said plainly. A spreadsheet sits in the catalogue
|
/* The waiting state, said plainly. A spreadsheet sits in the catalogue
|
||||||
@@ -107,6 +119,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'priced',
|
id: 'priced',
|
||||||
title: 'Price and release them',
|
title: 'Price and release them',
|
||||||
todo: 'A product with no price cannot be rung up, and one not released reaches no shop.',
|
todo: 'A product with no price cannot be rung up, and one not released reaches no shop.',
|
||||||
|
cta: 'Price and release',
|
||||||
done: priced.length > 0,
|
done: priced.length > 0,
|
||||||
href: '/admin/inventory',
|
href: '/admin/inventory',
|
||||||
...(products.length > 0 && priced.length < products.length
|
...(products.length > 0 && priced.length < products.length
|
||||||
@@ -117,6 +130,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'stocked',
|
id: 'stocked',
|
||||||
title: 'Put stock on the shelf',
|
title: 'Put stock on the shelf',
|
||||||
todo: 'Record what you actually hold — nothing sells at a balance of zero.',
|
todo: 'Record what you actually hold — nothing sells at a balance of zero.',
|
||||||
|
cta: 'Add stock',
|
||||||
done: stocked.length > 0,
|
done: stocked.length > 0,
|
||||||
href: '/admin/inventory',
|
href: '/admin/inventory',
|
||||||
...(priced.length > 0 && stocked.length < priced.length
|
...(priced.length > 0 && stocked.length < priced.length
|
||||||
@@ -127,6 +141,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
|
|||||||
id: 'onsale',
|
id: 'onsale',
|
||||||
title: 'See it in the app',
|
title: 'See it in the app',
|
||||||
todo: 'Once a product is priced, released, stocked and in a category, shoppers can buy it.',
|
todo: 'Once a product is priced, released, stocked and in a category, shoppers can buy it.',
|
||||||
|
cta: 'Check the app view',
|
||||||
done: onSale.length > 0,
|
done: onSale.length > 0,
|
||||||
href: '/admin/inventory',
|
href: '/admin/inventory',
|
||||||
...(onSale.length > 0 ? { detail: `${onSale.length} on sale` } : {}),
|
...(onSale.length > 0 ? { detail: `${onSale.length} on sale` } : {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user