/** * 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; onChange: (next: Partial) => 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 ( {!isTenantFixed ? ( // 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} onChange({ ...value, locationid: Number(next) || undefined })} /> {value.tenantid && branchOptions.length === 0 && !locations.isLoading ? ( This merchant has no branches, so there is nowhere for stock to land. Create one first. ) : null} ); } /** True when every field an import needs has been answered. */ export function isTargetComplete(value: Partial): 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): 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.`; }