live update
This commit is contained in:
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import { Badge } from '@astryxdesign/core/Badge';
|
||||
import { Button } from '@astryxdesign/core/Button';
|
||||
import { Card } from '@astryxdesign/core/Card';
|
||||
import { FileInput } from '@astryxdesign/core/FileInput';
|
||||
import { HStack } from '@astryxdesign/core/HStack';
|
||||
import { ProgressBar } from '@astryxdesign/core/ProgressBar';
|
||||
import { Table, type TableColumn } from '@astryxdesign/core/Table';
|
||||
@@ -12,6 +11,7 @@ import { AlertTriangle, CheckCircle2, Download, FileSpreadsheet } from 'lucide-r
|
||||
import { importSheetProducts, type SheetImportResult, type SheetProductRow } from '@/api/products';
|
||||
import { errorMessage } from '@/api/client';
|
||||
import { SectionHeader } from '@/components/SectionHeader';
|
||||
import { SheetDropzone } from '@/components/SheetDropzone';
|
||||
import { downloadTemplate, parseProductSheet, type ParsedSheet } from './parseProductSheet';
|
||||
|
||||
interface PreviewRow extends Record<string, unknown> {
|
||||
@@ -148,7 +148,7 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
if (result) {
|
||||
const isClean = result.failures.length === 0;
|
||||
return (
|
||||
<Card padding={4} elevation="low">
|
||||
<Card padding={4} variant="transparent">
|
||||
<VStack gap={3}>
|
||||
<HStack align="center" gap={1.5}>
|
||||
{isClean ? (
|
||||
@@ -197,7 +197,7 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
|
||||
return (
|
||||
<VStack gap={2}>
|
||||
<Card padding={0} elevation="low">
|
||||
<Card padding={0} variant="transparent">
|
||||
<VStack gap={2} padding={3}>
|
||||
<SectionHeader
|
||||
title="Upload the tenant's product list"
|
||||
@@ -212,17 +212,18 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
}
|
||||
/>
|
||||
|
||||
<FileInput
|
||||
label="Product workbook"
|
||||
value={file}
|
||||
onChange={handleFile}
|
||||
accept=".xlsx,.xls,.csv"
|
||||
mode="dropzone"
|
||||
placeholder="Drop an .xlsx or .csv here"
|
||||
description="Required columns: productname, productsku, categoryid, retailprice, productcost. The catalogue's category names do not map to a tenant's own ids, so categoryid must be in the file."
|
||||
isDisabled={!hasTarget}
|
||||
<SheetDropzone
|
||||
file={file}
|
||||
onFile={handleFile}
|
||||
blockedReason={hasTarget ? undefined : 'Pick a merchant and outlet first'}
|
||||
/>
|
||||
|
||||
<Text type="body" size="sm" style={{ color: 'var(--color-ink-3)' }}>
|
||||
Required columns: productname, productsku, categoryid, retailprice, productcost. The
|
||||
catalogue's category names do not map to a tenant's own ids, so categoryid must be in
|
||||
the file.
|
||||
</Text>
|
||||
|
||||
{parseError ? (
|
||||
<Text type="body" size="sm" style={{ color: 'var(--color-error, #d64545)' }}>
|
||||
{parseError}
|
||||
@@ -232,7 +233,7 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
</Card>
|
||||
|
||||
{parsed ? (
|
||||
<Card padding={0} elevation="low">
|
||||
<Card padding={0} variant="transparent">
|
||||
<VStack gap={2} padding={3}>
|
||||
<SectionHeader
|
||||
title="What is in the file"
|
||||
|
||||
@@ -120,7 +120,7 @@ export function GlobalCataloguePage() {
|
||||
<VStack gap={2}>
|
||||
{/* Only here. An upload is written against one merchant and one
|
||||
outlet, and the endpoint refuses a row without both. */}
|
||||
<Card padding={0} elevation="low">
|
||||
<Card padding={0} variant="transparent">
|
||||
<VStack gap={2} padding={2}>
|
||||
<SectionHeader
|
||||
title="Uploading for"
|
||||
|
||||
@@ -468,8 +468,8 @@ export function TillDrawer({
|
||||
isNew
|
||||
? cashierOnly
|
||||
? 'Add a cashier'
|
||||
: 'Add a till account'
|
||||
: (row.full_name ?? 'Edit till account')
|
||||
: 'Add a terminal account'
|
||||
: (row.full_name ?? 'Edit terminal account')
|
||||
}
|
||||
subtitle={
|
||||
isNew ? 'Signs in at a till — no console login' : `Till user ${row.user_id}`
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { Check, ChevronDown, Monitor, Store, Users } from 'lucide-react';
|
||||
import { AppShell, type MenuEntry, type NavEntry } from '@/components/shell/AppShell';
|
||||
import { BranchScopeProvider, useBranchScope } from './BranchScope';
|
||||
import { useLiveEvents } from '@/queries/useLiveEvents';
|
||||
|
||||
/**
|
||||
* Store Admin — the merchant workspace.
|
||||
@@ -42,7 +43,7 @@ const MANAGE: readonly MenuEntry[] = [
|
||||
to: '/admin/users',
|
||||
label: 'Users & access',
|
||||
icon: <Users size={13} />,
|
||||
note: 'Store and till accounts',
|
||||
note: 'Store and terminal accounts',
|
||||
},
|
||||
{
|
||||
to: '/admin/terminals',
|
||||
@@ -55,6 +56,7 @@ const MANAGE: readonly MenuEntry[] = [
|
||||
export function StoreAdminShell() {
|
||||
return (
|
||||
<BranchScopeProvider>
|
||||
<LiveWatch />
|
||||
<AppShell
|
||||
nav={NAV}
|
||||
home="/admin/console"
|
||||
@@ -66,6 +68,23 @@ export function StoreAdminShell() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the live stream for the branch currently in scope.
|
||||
*
|
||||
* A component rather than a hook call in the shell body, because it has to sit
|
||||
* INSIDE `BranchScopeProvider` to read the selection — and it renders nothing,
|
||||
* so where it sits in the tree costs nothing either.
|
||||
*
|
||||
* Under "All branches" `current` is undefined and no stream opens. That is
|
||||
* deliberate: the stream is per-outlet, and a six-branch tenant would otherwise
|
||||
* hold six connections open for a screen that is already polling.
|
||||
*/
|
||||
function LiveWatch() {
|
||||
const { tenantid, current } = useBranchScope();
|
||||
useLiveEvents(tenantid, current?.locationid);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The branch selector, in the header.
|
||||
*
|
||||
|
||||
@@ -987,7 +987,10 @@ function StatusTab({
|
||||
padding: '0 11px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${isActive ? 'transparent' : 'var(--color-line)'}`,
|
||||
background: isActive ? 'var(--color-brand)' : 'var(--color-surface)',
|
||||
// Transparent when idle, not white. A row of white chips on a tinted
|
||||
// page reads as six raised slabs; only the selected one should carry a
|
||||
// fill, and the hairline is enough to keep the rest legible as chips.
|
||||
background: isActive ? 'var(--color-brand)' : 'transparent',
|
||||
color: isActive ? '#fff' : 'var(--color-ink-2)',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -111,7 +111,7 @@ export function UsersPage() {
|
||||
description="Who can sign in to the console, and who can open a till."
|
||||
actions={
|
||||
<Button
|
||||
label={group === 'store' ? 'Add person' : 'Add till account'}
|
||||
label={group === 'store' ? 'Add person' : 'Add terminal account'}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
icon={<Plus size={14} />}
|
||||
@@ -137,7 +137,7 @@ export function UsersPage() {
|
||||
}}
|
||||
/>
|
||||
<TabButton
|
||||
label="Till accounts"
|
||||
label="Terminal accounts"
|
||||
icon={<Monitor size={14} />}
|
||||
badge={tillRows.length || undefined}
|
||||
isActive={group === 'till'}
|
||||
@@ -314,12 +314,12 @@ function TillTable({
|
||||
showBranch: boolean;
|
||||
onEdit: (row: PosUser, locationid: number) => void;
|
||||
}) {
|
||||
if (isLoading) return <Loading what="till accounts" />;
|
||||
if (isLoading) return <Loading what="terminal accounts" />;
|
||||
if (rows.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
icon={<Monitor size={22} />}
|
||||
title="No till accounts"
|
||||
title="No terminal accounts"
|
||||
body="Create a Supervisor first — they open the till on the device, and the terminal appears on the Console board once it reports in."
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AppShell, IconButton, type MenuEntry, type NavEntry } from '@/component
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { useTenantLocations } from '@/queries/hooks';
|
||||
import { BranchScopeProvider, useBranchScope } from '@/features/store-admin/BranchScope';
|
||||
import { useLiveEvents } from '@/queries/useLiveEvents';
|
||||
|
||||
/**
|
||||
* Store user — one branch, and only that branch.
|
||||
@@ -100,6 +101,7 @@ export function StoreUserShell() {
|
||||
|
||||
return (
|
||||
<BranchScopeProvider pin={locationid}>
|
||||
<LiveWatch />
|
||||
<AppShell
|
||||
nav={NAV}
|
||||
home="/store/console"
|
||||
@@ -203,3 +205,15 @@ function NoStore() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the live stream for this shop's outlet.
|
||||
*
|
||||
* Simpler than the Store Admin's: the branch is pinned, so there is always
|
||||
* exactly one outlet to watch and exactly one connection to hold.
|
||||
*/
|
||||
function LiveWatch() {
|
||||
const { tenantid, current } = useBranchScope();
|
||||
useLiveEvents(tenantid, current?.locationid);
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user