enhancement

This commit is contained in:
2026-09-07 16:46:47 +05:30
parent 05ac76f5f0
commit c31696ce41
35 changed files with 926 additions and 986 deletions

View File

@@ -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>
) : (