From b72bbd2f12689163584b8494b1a21d0fdd21acfe Mon Sep 17 00:00:00 2001 From: abhishek Date: Wed, 2 Sep 2026 10:48:47 +0530 Subject: [PATCH] changes --- src/App.tsx | 4 + src/features/setup/SetupPage.tsx | 302 +++++++++++++++++++++ src/features/setup/SetupTour.tsx | 57 ++-- src/features/setup/StoreAdminSetupPage.tsx | 35 +++ src/features/setup/StoreAdminTour.tsx | 61 +---- src/features/setup/StoreUserSetupPage.tsx | 35 +++ src/features/setup/StoreUserTour.tsx | 34 +-- src/features/setup/useSetupSteps.ts | 86 ++++++ src/features/store-admin/setupSteps.ts | 18 +- 9 files changed, 533 insertions(+), 99 deletions(-) create mode 100644 src/features/setup/SetupPage.tsx create mode 100644 src/features/setup/StoreAdminSetupPage.tsx create mode 100644 src/features/setup/StoreUserSetupPage.tsx create mode 100644 src/features/setup/useSetupSteps.ts diff --git a/src/App.tsx b/src/App.tsx index c41cc2e..3740703 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,8 @@ const UsersPage = named('UsersPage', () => import('@/features/store-admin/pages/ const TerminalsPage = named('TerminalsPage', () => import('@/features/store-admin/pages/TerminalsPage')); const AdminUploadsPage = named('UploadsPage', () => import('@/features/store-admin/pages/UploadsPage')); 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 branch by `BranchScopeProvider pin=`. Only what a shop does differently is @@ -126,6 +128,7 @@ export function App() { } /> } /> } /> + } /> {/* Catches `/admin/dashboard` and anything else that does not resolve. Without this, an unknown sub-path escapes to the global `*`, which redirects to this role's HOME_ROUTE — and if that is itself an @@ -157,6 +160,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/features/setup/SetupPage.tsx b/src/features/setup/SetupPage.tsx new file mode 100644 index 0000000..e911ea8 --- /dev/null +++ b/src/features/setup/SetupPage.tsx @@ -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 ( + + + + + Checking what is already done… + + + + ); + } + + return ( + + + + {/* 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. */} +
+
+
+ + + {steps.map((step, index) => ( + { + if (!isActive) onStart(); + navigate(step.href); + }} + onSkip={() => onSkip(step.id)} + /> + ))} + + + + {focus ? ( +
); diff --git a/src/features/setup/StoreAdminSetupPage.tsx b/src/features/setup/StoreAdminSetupPage.tsx new file mode 100644 index 0000000..62fbba9 --- /dev/null +++ b/src/features/setup/StoreAdminSetupPage.tsx @@ -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(() => readTour(userid, tenantid)); + + return ( + setTour(startTour(userid, tenantid))} + onSkip={(id) => setTour(skipInTour(userid, tenantid, id))} + onExit={() => { + setTour(endTour(userid, tenantid)); + navigate('/admin/console'); + }} + /> + ); +} diff --git a/src/features/setup/StoreAdminTour.tsx b/src/features/setup/StoreAdminTour.tsx index 026012a..7693913 100644 --- a/src/features/setup/StoreAdminTour.tsx +++ b/src/features/setup/StoreAdminTour.tsx @@ -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 * INSIDE `BranchScopeProvider` to read the branches — and because the two roles - * feed the tour from entirely different data, which a single shared mount would - * turn into a pile of conditionals. + * feed the walkthrough from entirely different data, which a single shared + * mount would turn into a pile of conditionals. * - * Every hook here already existed. The walkthrough needed no new backend at - * all, which is most of the reason it was worth building. + * Every hook it uses already existed. The walkthrough needed no new backend. */ -import { useMemo } from 'react'; -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 { useStoreAdminSteps } from './useSetupSteps'; import { SetupTour } from './SetupTour'; export function StoreAdminTour() { - 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 — 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; + const { steps, isLoading, tenantid, userid } = useStoreAdminSteps(); + if (isLoading || !userid) return null; return ( ); } diff --git a/src/features/setup/StoreUserSetupPage.tsx b/src/features/setup/StoreUserSetupPage.tsx new file mode 100644 index 0000000..96cc109 --- /dev/null +++ b/src/features/setup/StoreUserSetupPage.tsx @@ -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(() => readTour(userid, tenantid)); + + return ( + setTour(startTour(userid, tenantid))} + onSkip={(id) => setTour(skipInTour(userid, tenantid, id))} + onExit={() => { + setTour(endTour(userid, tenantid)); + navigate('/store/console'); + }} + /> + ); +} diff --git a/src/features/setup/StoreUserTour.tsx b/src/features/setup/StoreUserTour.tsx index 96b7905..39bc870 100644 --- a/src/features/setup/StoreUserTour.tsx +++ b/src/features/setup/StoreUserTour.tsx @@ -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, * hire anybody or edit the business, so walking them through those would be * showing somebody work they are not allowed to do. * * 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 - * on; a walkthrough on top of it would be a second thing to read and no second - * thing to do. + * meets "No store assigned", which is the whole of what they can act on. */ -import { useMemo } from 'react'; -import { useAuth } from '@/auth/AuthContext'; -import { useBranchScope } from '@/features/store-admin/BranchScope'; -import { useLocationProducts } from '@/queries/hooks'; -import { storeUserSteps } from './storeUserSteps'; +import { useStoreUserSteps } from './useSetupSteps'; import { SetupTour } from './SetupTour'; export function StoreUserTour() { - 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], - ); - - if (!user?.userid || !tenantid || !current?.locationid) return null; - if (products.isLoading) return null; + const { steps, isLoading, tenantid, userid } = useStoreUserSteps(); + if (isLoading || !userid) return null; return ( ); } diff --git a/src/features/setup/useSetupSteps.ts b/src/features/setup/useSetupSteps.ts new file mode 100644 index 0000000..e3f951b --- /dev/null +++ b/src/features/setup/useSetupSteps.ts @@ -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, + }; +} diff --git a/src/features/store-admin/setupSteps.ts b/src/features/store-admin/setupSteps.ts index 7f4baa8..ca69a2e 100644 --- a/src/features/store-admin/setupSteps.ts +++ b/src/features/store-admin/setupSteps.ts @@ -49,6 +49,16 @@ export interface SetupStep { href: string; /** A real count, when there is one worth showing. */ 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. @@ -101,7 +111,7 @@ export function setupSteps(input: SetupInput): SetupStep[] { gotcha: "A field left blank is not changed. Saving will never erase something you did not fill in.", 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', done: isProfileComplete(shop ?? {}), href: '/admin/profile', @@ -122,6 +132,9 @@ export function setupSteps(input: SetupInput): SetupStep[] { cta: 'Add a person', done: people.length > 0, href: '/admin/users', + ...(people.length > 0 + ? { doneNote: 'Your own account was created when the shop was set up.' } + : {}), ...(people.some(isUnplaced) ? { detail: `${people.filter(isUnplaced).length} not at a shop yet` } : {}), @@ -142,6 +155,9 @@ export function setupSteps(input: SetupInput): SetupStep[] { cta: 'Open a branch', done: branches.length > 0, 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}` } : {}), }, {