enhancement
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
||||
type IngestProduct,
|
||||
} from '@/api/ingest';
|
||||
import { uploadsApi, type UploadReceipt } from '@/api/uploads';
|
||||
import type { DateRange } from '@/api/insights';
|
||||
import type { SheetProductRow } from '@/api/products';
|
||||
import { readStoredRows } from './shelveRows';
|
||||
import { parseProductSheet } from '@/features/nearle-admin/import/parseProductSheet';
|
||||
@@ -75,6 +76,17 @@ export interface UploadsPanelProps {
|
||||
tenantid?: number;
|
||||
/** Narrow to one branch. A Store user is pinned to theirs. */
|
||||
locationid?: number;
|
||||
/**
|
||||
* The date range, owned by the page header.
|
||||
*
|
||||
* Every page in the console filters by date from the same control in the same
|
||||
* place; a second pair of date boxes inside one page's own toolbar meant this
|
||||
* screen alone asked the question twice, in a different shape, a few pixels
|
||||
* lower down.
|
||||
*/
|
||||
range?: DateRange;
|
||||
/** Clears the header's date filter, offered by the empty state. */
|
||||
onClearRange?: () => void;
|
||||
/**
|
||||
* Opens this workspace's upload flow.
|
||||
*
|
||||
@@ -92,15 +104,21 @@ interface LiveReading {
|
||||
products: IngestProduct[];
|
||||
}
|
||||
|
||||
export function UploadsPanel({ tenantid, locationid, onUpload }: UploadsPanelProps) {
|
||||
export function UploadsPanel({
|
||||
tenantid,
|
||||
locationid,
|
||||
range,
|
||||
onClearRange,
|
||||
onUpload,
|
||||
}: UploadsPanelProps) {
|
||||
const [receipts, setReceipts] = useState<UploadReceipt[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [live, setLive] = useState<Record<string, LiveReading>>({});
|
||||
const [detail, setDetail] = useState<string | null>(null);
|
||||
const [tab, setTab] = useState<UploadTab>('all');
|
||||
const [search, setSearch] = useState('');
|
||||
const [from, setFrom] = useState('');
|
||||
const [to, setTo] = useState('');
|
||||
const from = range?.fromdate ?? '';
|
||||
const to = range?.todate ?? '';
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -195,19 +213,29 @@ export function UploadsPanel({ tenantid, locationid, onUpload }: UploadsPanelPro
|
||||
});
|
||||
}, [rows, live, tab, search, from, to]);
|
||||
|
||||
/* Counts are of everything, not of what is on screen: a tab that reported the
|
||||
size of its own filtered view would always read the same as the list under
|
||||
it and could never tell you there is a failure worth looking at. */
|
||||
/*
|
||||
Counts ignore the TAB but respect every other filter.
|
||||
|
||||
Ignoring the tab is the point — a tab reporting the size of its own filtered
|
||||
view would always equal the list under it and could never tell you there is a
|
||||
failure worth looking at. Respecting the search and the date is equally the
|
||||
point, and was missed: with a date range that excluded everything, the list
|
||||
said "Nothing matches" while the tab beside it said two, and the two
|
||||
contradicted each other on the same screen.
|
||||
*/
|
||||
const counts = useMemo(() => {
|
||||
const out = { all: rows.length, done: 0, failed: 0, running: 0 };
|
||||
for (const receipt of rows) {
|
||||
const scoped = rows.filter(
|
||||
(receipt) => matchesSearch(receipt, search) && matchesDate(receipt, from, to),
|
||||
);
|
||||
const out = { all: scoped.length, done: 0, failed: 0, running: 0 };
|
||||
for (const receipt of scoped) {
|
||||
const state = stateOf(receipt, live[receipt.batchid]?.batch);
|
||||
if (matchesTab(state, 'done')) out.done += 1;
|
||||
if (matchesTab(state, 'failed')) out.failed += 1;
|
||||
if (matchesTab(state, 'running')) out.running += 1;
|
||||
}
|
||||
return out;
|
||||
}, [rows, live]);
|
||||
}, [rows, live, search, from, to]);
|
||||
|
||||
const open = shown.find((receipt) => receipt.batchid === detail) ?? null;
|
||||
const paged = usePaged(shown, { resetKey: `${tab}|${search}|${from}|${to}` });
|
||||
@@ -284,22 +312,6 @@ export function UploadsPanel({ tenantid, locationid, onUpload }: UploadsPanelPro
|
||||
aria-label="Search by file name"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="up-date"
|
||||
type="date"
|
||||
value={from}
|
||||
max={to || undefined}
|
||||
aria-label="Uploaded on or after"
|
||||
onChange={(event) => setFrom(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="up-date"
|
||||
type="date"
|
||||
value={to}
|
||||
min={from || undefined}
|
||||
aria-label="Uploaded on or before"
|
||||
onChange={(event) => setTo(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
label={isRefreshing ? 'Checking…' : 'Check again'}
|
||||
variant="secondary"
|
||||
@@ -323,13 +335,29 @@ export function UploadsPanel({ tenantid, locationid, onUpload }: UploadsPanelPro
|
||||
{/* ── The list ─────────────────────────────────────────────────────── */}
|
||||
{shown.length === 0 ? (
|
||||
<Card padding={4} variant="transparent">
|
||||
<VStack gap={0.5}>
|
||||
<VStack gap={1} align="start">
|
||||
<Text type="label" weight="semibold">
|
||||
Nothing matches
|
||||
</Text>
|
||||
{/*
|
||||
The date range is named as the likely cause, with the way out.
|
||||
This page opens on the month like every other, and a receipt log's
|
||||
most-wanted row is often older than that — an empty list that does
|
||||
not say which filter emptied it sends people to the refresh button.
|
||||
*/}
|
||||
<Text type="body" size="sm" color="secondary">
|
||||
No upload matches this tab, search or date range.
|
||||
{rows.length > 0 && (from || to)
|
||||
? `${rows.length} upload${rows.length === 1 ? ' is' : 's are'} outside the selected date range.`
|
||||
: 'No upload matches this tab, search or date range.'}
|
||||
</Text>
|
||||
{onClearRange && (from || to) ? (
|
||||
<Button
|
||||
label="Clear date filter"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={onClearRange}
|
||||
/>
|
||||
) : null}
|
||||
</VStack>
|
||||
</Card>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user