domain change
This commit is contained in:
149
src/features/import/ImportScope.tsx
Normal file
149
src/features/import/ImportScope.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Who an uploaded sheet is for.
|
||||
*
|
||||
* A sheet becomes stock on one shelf, so the upload has to name a tenant and a
|
||||
* branch before it can do anything — and where those come from depends entirely
|
||||
* on who is signed in:
|
||||
*
|
||||
* - A **Nearle Admin** works across every merchant. Nothing in their session
|
||||
* says which one, so they are ASKED, and the upload stays blocked until
|
||||
* they answer. Guessing here would put another merchant's products on a
|
||||
* shop's shelf.
|
||||
* - A **Store Admin or Store user** is already scoped by their login. Asking
|
||||
* them to pick their own tenant would be offering a choice with one legal
|
||||
* answer, and a chance to get it wrong.
|
||||
*
|
||||
* The branch matters as much as the tenant and is easier to forget: stock is
|
||||
* held per outlet, so a sheet uploaded against the wrong branch loads a shelf
|
||||
* nobody is standing at.
|
||||
*
|
||||
* A CATEGORY IS NOT ASKED FOR, and its absence is deliberate. It used to be the
|
||||
* third question here, and one answer was applied to every row in the sheet —
|
||||
* so a shop uploading its whole range had butter, bleach and biryani masala all
|
||||
* filed under the same aisle, and the app's category filter showed exactly that.
|
||||
* The category is now worked out per product from the catalogue team's ladder
|
||||
* (`productCategory.ts`) and resolved to an id at shelving time, which is a
|
||||
* classification and not a preference: there is nothing here for anyone to get
|
||||
* right or wrong.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Selector } from '@astryxdesign/core/Selector';
|
||||
import { Text } from '@astryxdesign/core/Text';
|
||||
import { VStack } from '@astryxdesign/core/VStack';
|
||||
import { useTenantLocations, useTenants } from '@/queries/hooks';
|
||||
|
||||
/** Everything an upload needs before it can be turned into stock. */
|
||||
export interface ImportTarget {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
}
|
||||
|
||||
export interface ImportScopeProps {
|
||||
value: Partial<ImportTarget>;
|
||||
onChange: (next: Partial<ImportTarget>) => void;
|
||||
/**
|
||||
* True for a store login, whose tenant is fixed by the session.
|
||||
*
|
||||
* The tenant picker is then not merely disabled but absent: a control that
|
||||
* cannot be used is still a question, and this one has no question in it.
|
||||
*/
|
||||
isTenantFixed?: boolean;
|
||||
}
|
||||
|
||||
export function ImportScope({ value, onChange, isTenantFixed = false }: ImportScopeProps) {
|
||||
const tenants = useTenants({ pageno: 1, pagesize: 200 });
|
||||
const locations = useTenantLocations(value.tenantid);
|
||||
|
||||
const tenantOptions = useMemo(
|
||||
() =>
|
||||
(tenants.data ?? [])
|
||||
.filter((tenant) => tenant.tenantid)
|
||||
.map((tenant) => ({
|
||||
value: String(tenant.tenantid),
|
||||
label: tenant.tenantname || `Tenant ${tenant.tenantid}`,
|
||||
})),
|
||||
[tenants.data],
|
||||
);
|
||||
|
||||
const branchOptions = useMemo(
|
||||
() =>
|
||||
(locations.data ?? []).map((branch) => ({
|
||||
value: String(branch.locationid),
|
||||
label: branch.locationname || `Branch ${branch.locationid}`,
|
||||
})),
|
||||
[locations.data],
|
||||
);
|
||||
|
||||
/**
|
||||
* A single branch is not a choice, so it is made rather than offered.
|
||||
*
|
||||
* Most merchants run one outlet. Leaving it unpicked would block the upload
|
||||
* behind a dropdown with one entry.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!value.tenantid) return;
|
||||
if (!value.locationid && branchOptions.length === 1) {
|
||||
onChange({ ...value, locationid: Number(branchOptions[0]!.value) });
|
||||
}
|
||||
}, [branchOptions, value, onChange]);
|
||||
|
||||
return (
|
||||
<VStack gap={1.5}>
|
||||
{!isTenantFixed ? (
|
||||
<Selector
|
||||
label="Merchant"
|
||||
size="sm"
|
||||
options={tenantOptions}
|
||||
value={value.tenantid ? String(value.tenantid) : ''}
|
||||
placeholder={tenants.isLoading ? 'Loading merchants…' : 'Choose a merchant'}
|
||||
description="Whose catalogue this sheet is loading. Nothing is uploaded until this is set."
|
||||
onChange={(next) =>
|
||||
// Changing the merchant clears the branch with it. Keeping it
|
||||
// would leave another tenant's branch id attached to this one — an
|
||||
// id that is valid-looking and wrong.
|
||||
onChange({ tenantid: Number(next) || undefined })
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Selector
|
||||
label="Branch"
|
||||
size="sm"
|
||||
options={branchOptions}
|
||||
value={value.locationid ? String(value.locationid) : ''}
|
||||
placeholder={
|
||||
!value.tenantid
|
||||
? 'Choose a merchant first'
|
||||
: locations.isLoading
|
||||
? 'Loading branches…'
|
||||
: 'Choose a branch'
|
||||
}
|
||||
description="Where the opening stock lands. Stock is held per branch, so this decides which shelf fills."
|
||||
isDisabled={!value.tenantid}
|
||||
onChange={(next) => onChange({ ...value, locationid: Number(next) || undefined })}
|
||||
/>
|
||||
|
||||
{value.tenantid && branchOptions.length === 0 && !locations.isLoading ? (
|
||||
<Text type="body" size="sm" style={{ color: 'var(--color-warning, #b7860b)' }}>
|
||||
This merchant has no branches, so there is nowhere for stock to land. Create one first.
|
||||
</Text>
|
||||
) : null}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
/** True when every field an import needs has been answered. */
|
||||
export function isTargetComplete(value: Partial<ImportTarget>): value is ImportTarget {
|
||||
return Boolean(value.tenantid && value.locationid);
|
||||
}
|
||||
|
||||
/** What is still missing, in words, for a blocked-reason line. */
|
||||
export function describeMissingTarget(value: Partial<ImportTarget>): string | null {
|
||||
const missing = [
|
||||
!value.tenantid ? 'a merchant' : null,
|
||||
!value.locationid ? 'a branch' : null,
|
||||
].filter(Boolean);
|
||||
if (missing.length === 0) return null;
|
||||
return `Choose ${missing.join(', ')} before uploading — a sheet becomes stock on one shelf, and this is which.`;
|
||||
}
|
||||
Reference in New Issue
Block a user