tenant profile
This commit is contained in:
@@ -23,19 +23,16 @@
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
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 { ArrowLeft, ArrowRight } from 'lucide-react';
|
||||
import { errorMessage } from '@/api/client';
|
||||
import { tenantsApi } from '@/api/tenants';
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { PageBody } from '@/components/PageBody';
|
||||
import { useBranchScope } from '@/features/store-admin/BranchScope';
|
||||
import { useLocationProducts, useOwnTenant } from '@/queries/hooks';
|
||||
import { StepFrame } from './StepFrame';
|
||||
import { Stepper } from './Stepper';
|
||||
import {
|
||||
completeStep,
|
||||
@@ -90,6 +87,7 @@ export function OnboardingPage() {
|
||||
const shop = useOwnTenant(tenantid || undefined);
|
||||
const products = useLocationProducts(tenantid || undefined, undefined, 0, { allBranches: true });
|
||||
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [state, setState] = useState<OnboardingState>(() => readOnboarding(userid, tenantid));
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -122,6 +120,29 @@ export function OnboardingPage() {
|
||||
});
|
||||
}, [shop.data]);
|
||||
|
||||
/**
|
||||
* Coming back from the errand, one step further on.
|
||||
*
|
||||
* A merchant sent to Inventory to add products used to have no way back into
|
||||
* setup but the browser's Back button — which returns them to the step they
|
||||
* already finished, so setup looked stuck. `SetupReturnBar` sends them here
|
||||
* with `?advance=<step>` instead, and this is the half that honours it.
|
||||
*
|
||||
* The parameter is consumed as it is read. Left in the URL it would re-fire on
|
||||
* every render and on a refresh, pushing somebody through steps they never
|
||||
* looked at.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const advance = params.get('advance') as StepId | null;
|
||||
if (!advance) return;
|
||||
const carry = new URLSearchParams(params);
|
||||
carry.delete('advance');
|
||||
setParams(carry, { replace: true });
|
||||
setState((prev) =>
|
||||
writeOnboarding(userid, tenantid, completeStep(prev, advance, nextOf(advance))),
|
||||
);
|
||||
}, [params, setParams, userid, tenantid]);
|
||||
|
||||
const productCount = (products.data ?? []).length;
|
||||
const step = resumeStep(state);
|
||||
const { done, total } = progressOf(state);
|
||||
@@ -215,25 +236,7 @@ export function OnboardingPage() {
|
||||
|
||||
return (
|
||||
<PageBody measure="reading">
|
||||
{!isFirst && !isLast ? (
|
||||
<VStack gap={3}>
|
||||
<VStack gap={0.5}>
|
||||
<Text
|
||||
type="display-1"
|
||||
weight="semibold"
|
||||
style={{ fontFamily: 'var(--font-display)', textWrap: 'balance' }}
|
||||
>
|
||||
{heading.title}
|
||||
</Text>
|
||||
{heading.blurb ? (
|
||||
<Text type="body" color="secondary">
|
||||
{heading.blurb}
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
<Stepper current={step} completed={state.completed} />
|
||||
</VStack>
|
||||
) : null}
|
||||
{!isFirst && !isLast ? <Stepper current={step} completed={state.completed} /> : null}
|
||||
|
||||
{step === 'welcome' ? (
|
||||
<WelcomeStep
|
||||
@@ -245,6 +248,19 @@ export function OnboardingPage() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isFirst && !isLast ? (
|
||||
<StepFrame
|
||||
step={step}
|
||||
title={heading.title}
|
||||
{...(heading.blurb ? { blurb: heading.blurb } : {})}
|
||||
continueLabel={step === 'review' ? 'Complete setup' : 'Save & continue'}
|
||||
isSaving={saving}
|
||||
canSkip={canSkip}
|
||||
onBack={() => goTo(backOf(step))}
|
||||
{...(canSkip ? { onSkip: () => persist(skipStep(state, step, nextOf(step))) } : {})}
|
||||
onContinue={() => void saveAndContinue()}
|
||||
onExit={() => navigate('/admin/console')}
|
||||
>
|
||||
{step === 'store' ? (
|
||||
<StoreInfoStep
|
||||
value={state.store}
|
||||
@@ -256,15 +272,18 @@ export function OnboardingPage() {
|
||||
{step === 'catalogue' ? (
|
||||
<CatalogueStep
|
||||
productCount={productCount}
|
||||
onUpload={() => navigate('/admin/inventory?tab=products&upload=1')}
|
||||
onManual={() => navigate('/admin/inventory')}
|
||||
onUpload={() => navigate('/admin/inventory?tab=products&upload=1&setup=catalogue')}
|
||||
/* The catalogue tab, not the products list. This pointed at
|
||||
`/admin/inventory` bare, which lands on Products — so "import from
|
||||
the catalogue" showed a merchant their own empty product list. */
|
||||
onManual={() => navigate('/admin/inventory?tab=catalogue&setup=catalogue')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{step === 'inventory' ? (
|
||||
<InventoryStep
|
||||
onDownloadTemplate={() => navigate('/admin/inventory')}
|
||||
onUpload={() => navigate('/admin/inventory')}
|
||||
onDownloadTemplate={() => navigate('/admin/inventory?tab=stock&setup=inventory')}
|
||||
onUpload={() => navigate('/admin/inventory?tab=products&upload=1&setup=inventory')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -288,57 +307,27 @@ export function OnboardingPage() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{step === 'done' ? (
|
||||
<DoneStep
|
||||
productCount={productCount}
|
||||
onCatalogue={() => navigate('/admin/inventory')}
|
||||
onInventory={() => navigate('/admin/inventory')}
|
||||
onStorefront={() => navigate('/admin/inventory')}
|
||||
onDashboard={() => navigate('/admin/console')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{saveError ? (
|
||||
<Card padding={3} variant="transparent">
|
||||
<Card padding={2} variant="transparent">
|
||||
<Text type="body" size="sm" role="alert" style={{ color: 'var(--color-error, #d64545)' }}>
|
||||
{saveError}
|
||||
</Text>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{/* Footer navigation. Back is quiet, Continue is the one action, and Skip
|
||||
sits between them so it is available without competing. */}
|
||||
{!isFirst && !isLast ? (
|
||||
<HStack justify="between" align="center" gap={2} wrap="wrap" style={{ paddingTop: 4 }}>
|
||||
<Button
|
||||
label="Back"
|
||||
variant="ghost"
|
||||
icon={<ArrowLeft size={14} />}
|
||||
isDisabled={saving}
|
||||
onClick={() => goTo(backOf(step))}
|
||||
/>
|
||||
<HStack gap={1.5} align="center" wrap="wrap">
|
||||
{canSkip ? (
|
||||
<Button
|
||||
label="Skip for now"
|
||||
variant="ghost"
|
||||
isDisabled={saving}
|
||||
onClick={() => persist(skipStep(state, step, nextOf(step)))}
|
||||
/>
|
||||
) : null}
|
||||
<Button
|
||||
label={
|
||||
saving ? 'Saving…' : step === 'review' ? 'Complete setup' : 'Save & continue'
|
||||
}
|
||||
variant="primary"
|
||||
isLoading={saving}
|
||||
isDisabled={saving}
|
||||
endContent={<ArrowRight size={14} />}
|
||||
onClick={() => void saveAndContinue()}
|
||||
/>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</StepFrame>
|
||||
) : null}
|
||||
|
||||
{step === 'done' ? (
|
||||
<DoneStep
|
||||
productCount={productCount}
|
||||
onCatalogue={() => navigate('/admin/inventory?tab=catalogue')}
|
||||
onInventory={() => navigate('/admin/inventory?tab=stock')}
|
||||
onStorefront={() => navigate('/admin/inventory?tab=products')}
|
||||
onDashboard={() => navigate('/admin/console')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user