This commit is contained in:
2026-09-02 10:48:47 +05:30
parent 98712493c4
commit b72bbd2f12
9 changed files with 533 additions and 99 deletions

View File

@@ -43,6 +43,8 @@ const UsersPage = named('UsersPage', () => import('@/features/store-admin/pages/
const TerminalsPage = named('TerminalsPage', () => import('@/features/store-admin/pages/TerminalsPage')); const TerminalsPage = named('TerminalsPage', () => import('@/features/store-admin/pages/TerminalsPage'));
const AdminUploadsPage = named('UploadsPage', () => import('@/features/store-admin/pages/UploadsPage')); const AdminUploadsPage = named('UploadsPage', () => import('@/features/store-admin/pages/UploadsPage'));
const ShopProfilePage = named('ShopProfilePage', () => import('@/features/store-admin/pages/ShopProfilePage')); const ShopProfilePage = named('ShopProfilePage', () => import('@/features/store-admin/pages/ShopProfilePage'));
const StoreAdminSetupPage = named('StoreAdminSetupPage', () => import('@/features/setup/StoreAdminSetupPage'));
const StoreUserSetupPage = named('StoreUserSetupPage', () => import('@/features/setup/StoreUserSetupPage'));
/* The Store user workspace reuses the merchant's four pages, pinned to one /* The Store user workspace reuses the merchant's four pages, pinned to one
branch by `BranchScopeProvider pin=`. Only what a shop does differently is branch by `BranchScopeProvider pin=`. Only what a shop does differently is
@@ -126,6 +128,7 @@ export function App() {
<Route path="terminals" element={<TerminalsPage />} /> <Route path="terminals" element={<TerminalsPage />} />
<Route path="uploads" element={<AdminUploadsPage />} /> <Route path="uploads" element={<AdminUploadsPage />} />
<Route path="profile" element={<ShopProfilePage />} /> <Route path="profile" element={<ShopProfilePage />} />
<Route path="setup" element={<StoreAdminSetupPage />} />
{/* Catches `/admin/dashboard` and anything else that does not resolve. {/* Catches `/admin/dashboard` and anything else that does not resolve.
Without this, an unknown sub-path escapes to the global `*`, which Without this, an unknown sub-path escapes to the global `*`, which
redirects to this role's HOME_ROUTE — and if that is itself an redirects to this role's HOME_ROUTE — and if that is itself an
@@ -157,6 +160,7 @@ export function App() {
<Route path="terminals" element={<TerminalsPage />} /> <Route path="terminals" element={<TerminalsPage />} />
<Route path="staff" element={<StoreStaffPage />} /> <Route path="staff" element={<StoreStaffPage />} />
<Route path="uploads" element={<StoreUploadsPage />} /> <Route path="uploads" element={<StoreUploadsPage />} />
<Route path="setup" element={<StoreUserSetupPage />} />
<Route path="account" element={<StoreAccountPage />} /> <Route path="account" element={<StoreAccountPage />} />
<Route path="*" element={<Navigate to="/store/console" replace />} /> <Route path="*" element={<Navigate to="/store/console" replace />} />
</Route> </Route>

View File

@@ -0,0 +1,302 @@
/**
* The whole journey from "signed in" to "selling", on one screen.
*
* The walkthrough started as a strip along the top of the console, and a strip
* is the wrong shape for this. It can show one step, so a merchant sees a
* sentence and a button with no idea what they have agreed to, how much is
* left, or why the thing they just finished was followed by something they were
* never shown. Onboarding completes two of the seven steps before anybody signs
* in, and the strip leapt over both in silence.
*
* So the journey gets a page. Every step is visible with its real state, the
* current one is open with its guidance, and a step that was finished before
* the merchant arrived says so instead of vanishing. The strip stays, but only
* as a way back here while the work is being done on another screen.
*
* Nothing here is stored. Every tick is derived from live data, so the page
* cannot claim work that was not done, and re-reading it after a change is what
* moves it on.
*/
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@astryxdesign/core/Button';
import { Card } from '@astryxdesign/core/Card';
import { HStack } from '@astryxdesign/core/HStack';
import { Text } from '@astryxdesign/core/Text';
import { VStack } from '@astryxdesign/core/VStack';
import { ArrowRight, Check, Info } from 'lucide-react';
import { PageBody } from '@/components/PageBody';
import { PageHeader } from '@/components/PageHeader';
import type { SetupStep } from '@/features/store-admin/setupSteps';
export interface SetupPageProps {
steps: readonly SetupStep[];
/** Steps the merchant has waved past. Never counted as done. */
skipped: readonly string[];
isLoading?: boolean;
home: string;
onStart: () => void;
onSkip: (id: string) => void;
onExit: () => void;
/** True while the walkthrough is running, which changes the primary action. */
isActive: boolean;
}
export function SetupPage({
steps,
skipped,
isLoading = false,
home,
onStart,
onSkip,
onExit,
isActive,
}: SetupPageProps) {
const navigate = useNavigate();
const focus = useMemo(
() => steps.find((step) => !step.done && !skipped.includes(step.id)) ?? null,
[steps, skipped],
);
const done = steps.filter((step) => step.done).length;
const pct = steps.length === 0 ? 0 : Math.round((done / steps.length) * 100);
if (isLoading) {
return (
<PageBody measure="reading">
<PageHeader title="Set up your shop" />
<Card padding={4} elevation="low">
<Text type="body" color="secondary">
Checking what is already done…
</Text>
</Card>
</PageBody>
);
}
return (
<PageBody measure="reading">
<PageHeader
title="Set up your shop"
count={`${done} of ${steps.length}`}
description={
focus
? 'Work through these and your products will be on sale in the app. You can skip any of them and come back.'
: 'Everything is done — your shop is set up.'
}
/>
{/* Progress as a bar, not just a fraction. "2 of 7" tells somebody how far
they are; a bar tells them at a glance whether this is nearly over. */}
<div
aria-hidden
style={{
height: 4,
borderRadius: 999,
background: 'var(--color-line)',
overflow: 'hidden',
}}
>
<div
style={{
width: `${pct}%`,
height: '100%',
background: 'var(--color-brand)',
transition: 'width .3s ease',
}}
/>
</div>
<VStack gap={0}>
{steps.map((step, index) => (
<StepRow
key={step.id}
step={step}
index={index}
isLast={index === steps.length - 1}
isFocus={step.id === focus?.id}
isSkipped={!step.done && skipped.includes(step.id)}
isActive={isActive}
onGo={() => {
if (!isActive) onStart();
navigate(step.href);
}}
onSkip={() => onSkip(step.id)}
/>
))}
</VStack>
<HStack gap={1.5} wrap="wrap" style={{ paddingTop: 8 }}>
{focus ? (
<Button
label={isActive ? `Continue — ${focus.cta}` : 'Start setup'}
variant="primary"
endContent={<ArrowRight size={14} />}
onClick={() => {
if (!isActive) onStart();
navigate(focus.href);
}}
/>
) : (
<Button label="Go to the console" variant="primary" onClick={() => navigate(home)} />
)}
{isActive ? <Button label="Finish later" variant="ghost" onClick={onExit} /> : null}
</HStack>
</PageBody>
);
}
/* ── One step in the journey ──────────────────────────────────────────────── */
function StepRow({
step,
index,
isLast,
isFocus,
isSkipped,
isActive,
onGo,
onSkip,
}: {
step: SetupStep;
index: number;
isLast: boolean;
isFocus: boolean;
isSkipped: boolean;
isActive: boolean;
onGo: () => void;
onSkip: () => void;
}) {
const marker = step.done
? { bg: 'var(--color-success, #10b981)', fg: '#fff', ring: 'transparent' }
: isFocus
? { bg: 'var(--color-brand)', fg: '#fff', ring: 'transparent' }
: { bg: 'transparent', fg: 'var(--color-ink-4)', ring: 'var(--color-line)' };
return (
<HStack gap={2} align="start">
{/* The rail: a numbered marker with a line joining it to the next, so the
seven read as one sequence rather than seven cards. */}
<VStack gap={0} align="center" style={{ flex: 'none', width: 28 }}>
<span
style={{
width: 28,
height: 28,
borderRadius: 999,
display: 'grid',
placeItems: 'center',
background: marker.bg,
color: marker.fg,
border: `1px solid ${marker.ring}`,
fontSize: 12,
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
}}
>
{step.done ? <Check size={14} /> : index + 1}
</span>
{!isLast ? (
<span
aria-hidden
style={{
width: 1,
flex: 1,
minHeight: isFocus ? 120 : 28,
background: 'var(--color-line)',
}}
/>
) : null}
</VStack>
<VStack gap={0.5} style={{ flex: 1, minWidth: 0, paddingBottom: isLast ? 0 : 18 }}>
<HStack gap={1} align="center" wrap="wrap">
<Text
type="label"
size={isFocus ? 'lg' : 'sm'}
weight="semibold"
{...(step.done || isSkipped ? { color: 'secondary' as const } : {})}
>
{step.title}
</Text>
{step.detail ? (
<Text type="body" size="xsm" color="secondary" hasTabularNumbers>
{step.detail}
</Text>
) : null}
{isSkipped ? (
<Text type="body" size="xsm" color="secondary">
· skipped
</Text>
) : null}
</HStack>
{/* A step finished before the merchant arrived says why. Two of the
seven are done by onboarding, and silently ticking them looks like
the walkthrough inventing progress. */}
{step.done && step.doneNote ? (
<Text type="body" size="xsm" color="secondary">
{step.doneNote}
</Text>
) : null}
{/* Only the current step opens. Seven expanded panels is a manual, not
a walkthrough. */}
{isFocus ? (
<VStack gap={1.5} style={{ paddingTop: 6 }}>
<Text type="body" size="sm" color="secondary" style={{ lineHeight: 1.65 }}>
{step.why}
</Text>
<VStack gap={0.5}>
<Text
type="label"
size="xsm"
color="secondary"
style={{ textTransform: 'uppercase', letterSpacing: '0.09em' }}
>
What to do
</Text>
<ol style={{ margin: 0, paddingLeft: 20, display: 'grid', gap: 4 }}>
{step.how.map((line) => (
<li key={line}>
<Text type="body" size="sm" style={{ lineHeight: 1.6 }}>
{line}
</Text>
</li>
))}
</ol>
</VStack>
{step.gotcha ? (
<HStack gap={1} align="start">
<Info
size={14}
style={{ color: 'var(--color-warning, #b7860b)', flex: 'none', marginTop: 3 }}
/>
<Text type="body" size="sm" color="secondary" style={{ lineHeight: 1.6 }}>
{step.gotcha}
</Text>
</HStack>
) : null}
<HStack gap={1} align="center" wrap="wrap">
<Button
label={step.cta}
variant="primary"
size="sm"
endContent={<ArrowRight size={13} />}
onClick={onGo}
/>
{/* Skipping sits after the action, never before it. */}
{isActive ? (
<Button label="Skip this step" variant="ghost" size="sm" onClick={onSkip} />
) : null}
</HStack>
</VStack>
) : null}
</VStack>
</HStack>
);
}

View File

@@ -43,9 +43,11 @@ export interface SetupTourProps {
steps: readonly SetupStep[]; steps: readonly SetupStep[];
/** Where "finished" lands. The console, for both roles. */ /** Where "finished" lands. The console, for both roles. */
home: string; home: string;
/** The journey page — where the walkthrough starts and can be reviewed. */
setupHref: string;
} }
export function SetupTour({ userid, tenantid, steps, home }: SetupTourProps) { export function SetupTour({ userid, tenantid, steps, home, setupHref }: SetupTourProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
const [tour, setTour] = useState<TourState>(() => readTour(userid, tenantid)); const [tour, setTour] = useState<TourState>(() => readTour(userid, tenantid));
@@ -102,6 +104,10 @@ export function SetupTour({ userid, tenantid, steps, home }: SetupTourProps) {
onStart={() => { onStart={() => {
walkedTo.current = null; walkedTo.current = null;
setTour(startTour(userid, tenantid)); setTour(startTour(userid, tenantid));
// To the journey first, not to step one. Somebody agreeing to a
// walkthrough should see what they agreed to — how many steps,
// which are already done, and why.
navigate(setupHref);
}} }}
onCancel={() => setTour(declineTour(userid, tenantid))} onCancel={() => setTour(declineTour(userid, tenantid))}
/> />
@@ -128,7 +134,7 @@ export function SetupTour({ userid, tenantid, steps, home }: SetupTourProps) {
justify="between" justify="between"
gap={2} gap={2}
wrap="wrap" wrap="wrap"
style={{ padding: '10px 0' }} style={{ padding: '10px 0 0' }}
> >
<HStack gap={1.5} align="center" style={{ minWidth: 0 }}> <HStack gap={1.5} align="center" style={{ minWidth: 0 }}>
<Sparkles size={15} style={{ color: 'var(--color-brand)', flex: 'none' }} /> <Sparkles size={15} style={{ color: 'var(--color-brand)', flex: 'none' }} />
@@ -148,6 +154,12 @@ export function SetupTour({ userid, tenantid, steps, home }: SetupTourProps) {
between the header and the work on every screen. Open, it stays between the header and the work on every screen. Open, it stays
open across steps — somebody who wants the detail wants it for open across steps — somebody who wants the detail wants it for
all of them. */} all of them. */}
<Button
label="All steps"
variant="ghost"
size="sm"
onClick={() => navigate(setupHref)}
/>
<Button <Button
label={isOpen ? 'Hide guide' : 'How do I do this?'} label={isOpen ? 'Hide guide' : 'How do I do this?'}
variant="ghost" variant="ghost"
@@ -167,27 +179,34 @@ export function SetupTour({ userid, tenantid, steps, home }: SetupTourProps) {
onClick={() => navigate(focus.href)} onClick={() => navigate(focus.href)}
/> />
) : null} ) : null}
<Button
label="Skip this step"
variant="ghost"
size="sm"
onClick={() => setTour(skipInTour(userid, tenantid, focus.id))}
/>
{/* Leaving is always available. A walkthrough somebody cannot get
out of is a trap, and the offer is not made again anyway. */}
<Button
label="Exit setup"
variant="ghost"
size="sm"
onClick={() => {
setTour(endTour(userid, tenantid));
navigate(home);
}}
/>
</HStack> </HStack>
</HStack> </HStack>
{isOpen ? <StepGuide step={focus} /> : null} {isOpen ? <StepGuide step={focus} /> : null}
{/* Skip and Exit sit at the END, bottom right.
They were beside the action at the top, where the eye lands first —
so the two ways OUT of a step were more prominent than the one way
through it. Leaving is offered, not advertised. */}
<HStack justify="end" gap={1} style={{ paddingBottom: 10 }}>
<Button
label="Skip this step"
variant="ghost"
size="sm"
onClick={() => setTour(skipInTour(userid, tenantid, focus.id))}
/>
{/* Leaving is always available. A walkthrough somebody cannot get
out of is a trap, and the offer is not made again anyway. */}
<Button
label="Exit setup"
variant="ghost"
size="sm"
onClick={() => {
setTour(endTour(userid, tenantid));
navigate(home);
}}
/>
</HStack>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,35 @@
/**
* The merchant's setup journey, at /admin/setup.
*
* Reachable at any time, not only during the walkthrough — somebody who
* dismissed the offer, or finished half of it last week, needs a way back to
* the list without being asked again.
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { SetupPage } from './SetupPage';
import { useStoreAdminSteps } from './useSetupSteps';
import { endTour, readTour, skipInTour, startTour, type TourState } from './tourState';
export function StoreAdminSetupPage() {
const navigate = useNavigate();
const { steps, isLoading, tenantid, userid } = useStoreAdminSteps();
const [tour, setTour] = useState<TourState>(() => readTour(userid, tenantid));
return (
<SetupPage
steps={steps}
skipped={tour.skipped}
isLoading={isLoading}
isActive={tour.active}
home="/admin/console"
onStart={() => setTour(startTour(userid, tenantid))}
onSkip={(id) => setTour(skipInTour(userid, tenantid, id))}
onExit={() => {
setTour(endTour(userid, tenantid));
navigate('/admin/console');
}}
/>
);
}

View File

@@ -1,71 +1,28 @@
/** /**
* The merchant's walkthrough, mounted in the Store Admin shell. * The merchant's walkthrough strip, mounted in the Store Admin shell.
* *
* Its own component rather than props on the shell, because it has to sit * Its own component rather than props on the shell, because it has to sit
* INSIDE `BranchScopeProvider` to read the branches — and because the two roles * INSIDE `BranchScopeProvider` to read the branches — and because the two roles
* feed the tour from entirely different data, which a single shared mount would * feed the walkthrough from entirely different data, which a single shared
* turn into a pile of conditionals. * mount would turn into a pile of conditionals.
* *
* Every hook here already existed. The walkthrough needed no new backend at * Every hook it uses already existed. The walkthrough needed no new backend.
* all, which is most of the reason it was worth building.
*/ */
import { useMemo } from 'react'; import { useStoreAdminSteps } from './useSetupSteps';
import { useAuth } from '@/auth/AuthContext';
import { useBranchScope } from '@/features/store-admin/BranchScope';
import { setupSteps } from '@/features/store-admin/setupSteps';
import {
useLocationProducts,
useOwnTenant,
useStaff,
useUploads,
} from '@/queries/hooks';
import { SetupTour } from './SetupTour'; import { SetupTour } from './SetupTour';
export function StoreAdminTour() { export function StoreAdminTour() {
const { user } = useAuth(); const { steps, isLoading, tenantid, userid } = useStoreAdminSteps();
const { branches, tenantid } = useBranchScope(); if (isLoading || !userid) return null;
const shop = useOwnTenant(tenantid || undefined);
const people = useStaff(tenantid || undefined);
const products = useLocationProducts(tenantid || undefined, undefined, 0, { allBranches: true });
const uploads = useUploads(tenantid || undefined);
/* Drops the catalogue service has not released yet. Without this the
products step reads as neglected when it is simply not our turn — the
sheet is sitting in their review queue. */
const pendingUploads = useMemo(
() =>
(uploads.data ?? []).filter((receipt) => receipt.laststatus === 'pending' && !receipt.runid)
.length,
[uploads.data],
);
const steps = useMemo(
() =>
setupSteps({
shop: shop.data,
people: people.data ?? [],
branches,
products: products.data ?? [],
pendingUploads,
}),
[shop.data, people.data, branches, products.data, pendingUploads],
);
/* Nothing is offered until the data has actually arrived. Every step reads
as undone while the queries are in flight, so a tour started then would
walk somebody through work they had already finished. */
const isReady =
Boolean(tenantid) && !shop.isLoading && !people.isLoading && !products.isLoading;
if (!isReady || !user?.userid) return null;
return ( return (
<SetupTour <SetupTour
userid={user.userid} userid={userid}
tenantid={tenantid} tenantid={tenantid}
steps={steps} steps={steps}
home="/admin/console" home="/admin/console"
setupHref="/admin/setup"
/> />
); );
} }

View File

@@ -0,0 +1,35 @@
/**
* The branch user’s setup journey, at /store/setup.
*
* Reachable at any time, not only during the walkthrough — somebody who
* dismissed the offer, or finished half of it last week, needs a way back to
* the list without being asked again.
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { SetupPage } from './SetupPage';
import { useStoreUserSteps } from './useSetupSteps';
import { endTour, readTour, skipInTour, startTour, type TourState } from './tourState';
export function StoreUserSetupPage() {
const navigate = useNavigate();
const { steps, isLoading, tenantid, userid } = useStoreUserSteps();
const [tour, setTour] = useState<TourState>(() => readTour(userid, tenantid));
return (
<SetupPage
steps={steps}
skipped={tour.skipped}
isLoading={isLoading}
isActive={tour.active}
home="/store/console"
onStart={() => setTour(startTour(userid, tenantid))}
onSkip={(id) => setTour(skipInTour(userid, tenantid, id))}
onExit={() => {
setTour(endTour(userid, tenantid));
navigate('/store/console');
}}
/>
);
}

View File

@@ -1,48 +1,28 @@
/** /**
* The branch user's walkthrough, mounted in the Store user shell. * The branch user's walkthrough strip.
* *
* Three steps, not seven, and honestly so — a counter user cannot open outlets, * Three steps, not seven, and honestly so — a counter user cannot open outlets,
* hire anybody or edit the business, so walking them through those would be * hire anybody or edit the business, so walking them through those would be
* showing somebody work they are not allowed to do. * showing somebody work they are not allowed to do.
* *
* Nothing is offered until they have a branch. An unassigned account already * Nothing is offered until they have a branch. An unassigned account already
* meets the "No store assigned" screen, which is the whole of what they can act * meets "No store assigned", which is the whole of what they can act on.
* on; a walkthrough on top of it would be a second thing to read and no second
* thing to do.
*/ */
import { useMemo } from 'react'; import { useStoreUserSteps } from './useSetupSteps';
import { useAuth } from '@/auth/AuthContext';
import { useBranchScope } from '@/features/store-admin/BranchScope';
import { useLocationProducts } from '@/queries/hooks';
import { storeUserSteps } from './storeUserSteps';
import { SetupTour } from './SetupTour'; import { SetupTour } from './SetupTour';
export function StoreUserTour() { export function StoreUserTour() {
const { user } = useAuth(); const { steps, isLoading, tenantid, userid } = useStoreUserSteps();
const { current, tenantid } = useBranchScope(); if (isLoading || !userid) return null;
const products = useLocationProducts(tenantid || undefined, current?.locationid, 0);
const steps = useMemo(
() =>
storeUserSteps({
user,
hasBranch: Boolean(current?.locationid),
productCount: (products.data ?? []).length,
...(current?.locationname ? { branchName: current.locationname } : {}),
}),
[user, current?.locationid, current?.locationname, products.data],
);
if (!user?.userid || !tenantid || !current?.locationid) return null;
if (products.isLoading) return null;
return ( return (
<SetupTour <SetupTour
userid={user.userid} userid={userid}
tenantid={tenantid} tenantid={tenantid}
steps={steps} steps={steps}
home="/store/console" home="/store/console"
setupHref="/store/setup"
/> />
); );
} }

View File

@@ -0,0 +1,86 @@
/**
* The steps for each role, in one place.
*
* Both the journey page and the strip along the top need them, computed
* identically — two copies of this would drift, and the first symptom would be
* a page saying a step is finished while the strip still asks for it.
*/
import { useMemo } from 'react';
import { useAuth } from '@/auth/AuthContext';
import { useBranchScope } from '@/features/store-admin/BranchScope';
import { setupSteps, type SetupStep } from '@/features/store-admin/setupSteps';
import { useLocationProducts, useOwnTenant, useStaff, useUploads } from '@/queries/hooks';
import { storeUserSteps } from './storeUserSteps';
export interface RoleSteps {
steps: SetupStep[];
isLoading: boolean;
tenantid: number;
userid: number;
}
export function useStoreAdminSteps(): RoleSteps {
const { user } = useAuth();
const { branches, tenantid } = useBranchScope();
const shop = useOwnTenant(tenantid || undefined);
const people = useStaff(tenantid || undefined);
const products = useLocationProducts(tenantid || undefined, undefined, 0, { allBranches: true });
const uploads = useUploads(tenantid || undefined);
/* Drops the catalogue service has not released yet. Without this the products
step reads as neglected when it is simply not our turn. */
const pendingUploads = useMemo(
() =>
(uploads.data ?? []).filter((receipt) => receipt.laststatus === 'pending' && !receipt.runid)
.length,
[uploads.data],
);
const steps = useMemo(
() =>
setupSteps({
shop: shop.data,
people: people.data ?? [],
branches,
products: products.data ?? [],
pendingUploads,
}),
[shop.data, people.data, branches, products.data, pendingUploads],
);
return {
steps,
// Nothing is judged until the data has arrived: every step reads as undone
// while the queries are in flight, and acting on that would walk somebody
// through work they had already finished.
isLoading: !tenantid || shop.isLoading || people.isLoading || products.isLoading,
tenantid,
userid: user?.userid ?? 0,
};
}
export function useStoreUserSteps(): RoleSteps {
const { user } = useAuth();
const { current, tenantid } = useBranchScope();
const products = useLocationProducts(tenantid || undefined, current?.locationid, 0);
const steps = useMemo(
() =>
storeUserSteps({
user,
hasBranch: Boolean(current?.locationid),
productCount: (products.data ?? []).length,
...(current?.locationname ? { branchName: current.locationname } : {}),
}),
[user, current?.locationid, current?.locationname, products.data],
);
return {
steps,
isLoading: !tenantid || !current?.locationid || products.isLoading,
tenantid,
userid: user?.userid ?? 0,
};
}

View File

@@ -49,6 +49,16 @@ export interface SetupStep {
href: string; href: string;
/** A real count, when there is one worth showing. */ /** A real count, when there is one worth showing. */
detail?: string; detail?: string;
/**
* Why a step was already finished before anybody started.
*
* Onboarding creates a tenant, its first branch AND its administrator in
* one transaction, so two of the seven are done before a merchant ever
* signs in. The walkthrough used to leap straight past them, which reads
* as the tour skipping steps by itself. Saying what happened costs one
* line and removes the whole confusion.
*/
doneNote?: string;
/** /**
* Why this step is worth doing — the consequence of not doing it. * Why this step is worth doing — the consequence of not doing it.
@@ -101,7 +111,7 @@ export function setupSteps(input: SetupInput): SetupStep[] {
gotcha: gotcha:
"A field left blank is not changed. Saving will never erase something you did not fill in.", "A field left blank is not changed. Saving will never erase something you did not fill in.",
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, licence and a line about what you sell.',
cta: 'Add your shop details', cta: 'Add your shop details',
done: isProfileComplete(shop ?? {}), done: isProfileComplete(shop ?? {}),
href: '/admin/profile', href: '/admin/profile',
@@ -122,6 +132,9 @@ export function setupSteps(input: SetupInput): SetupStep[] {
cta: 'Add a person', cta: 'Add a person',
done: people.length > 0, done: people.length > 0,
href: '/admin/users', href: '/admin/users',
...(people.length > 0
? { doneNote: 'Your own account was created when the shop was set up.' }
: {}),
...(people.some(isUnplaced) ...(people.some(isUnplaced)
? { detail: `${people.filter(isUnplaced).length} not at a shop yet` } ? { detail: `${people.filter(isUnplaced).length} not at a shop yet` }
: {}), : {}),
@@ -142,6 +155,9 @@ export function setupSteps(input: SetupInput): SetupStep[] {
cta: 'Open a branch', cta: 'Open a branch',
done: branches.length > 0, done: branches.length > 0,
href: '/admin/branches/new', href: '/admin/branches/new',
...(branches.length > 0
? { doneNote: 'Your first outlet was opened when the shop was set up.' }
: {}),
...(branches.length > 0 ? { detail: `${branches.length}` } : {}), ...(branches.length > 0 ? { detail: `${branches.length}` } : {}),
}, },
{ {