This commit is contained in:
2026-09-07 17:27:27 +05:30
parent c31696ce41
commit 954dfb8c00
6 changed files with 671 additions and 24 deletions

View File

@@ -26,6 +26,11 @@ import { importSheetProducts, type SheetImportResult, type SheetProductRow } fro
import { errorMessage } from '@/api/client';
import { SectionHeader } from '@/components/SectionHeader';
import { SheetDropzone } from '@/components/SheetDropzone';
import {
resolveCategory,
UNKNOWN_CATEGORY,
type CategoryRule,
} from './productCategory';
import { TablePager } from '@/components/TablePager';
import { usePaged } from '@/components/usePaged';
import {
@@ -34,10 +39,20 @@ import {
type ParsedSheet,
} from '@/features/nearle-admin/import/parseProductSheet';
/** How the category was arrived at, in the operator's words. */
const RULE_LABEL: Record<CategoryRule, string> = {
sheet: 'from your sheet',
registry: 'matched by name',
lexicon: 'matched as a commodity',
unknown: 'could not tell',
};
interface PreviewRow extends Record<string, unknown> {
productname: string;
productsku: string;
categoryid: number;
/** Resolved by the category ladder, before anything is created. */
category: string;
categoryRule: CategoryRule;
retailprice: number;
productcost: number;
quantity: number;
@@ -119,11 +134,25 @@ export function TenantSheetImportPanel({ tenantid, locationid }: TenantSheetImpo
),
},
{
key: 'categoryid',
key: 'category',
header: 'Category',
align: 'end',
width: { type: 'pixel', value: 100 },
renderCell: (row) => <Text type="body" size="sm" hasTabularNumbers>{row.categoryid}</Text>,
width: { type: 'pixel', value: 170 },
renderCell: (row) => (
<VStack gap={0}>
<Text
type="body"
size="sm"
{...(row.category === UNKNOWN_CATEGORY ? { color: 'secondary' as const } : {})}
>
{row.category}
</Text>
{/* Which rule answered. An operator trusts their own column and
checks an inferred one, and cannot tell them apart otherwise. */}
<Text type="body" size="xsm" color="secondary">
{RULE_LABEL[row.categoryRule]}
</Text>
</VStack>
),
},
{
key: 'retailprice',
@@ -159,14 +188,35 @@ export function TenantSheetImportPanel({ tenantid, locationid }: TenantSheetImpo
/* The whole sheet, paged — it used to be cut at 25 with nothing saying so,
unlike the issues list above, which says '…and N more'. A bad row on line
300 was unreachable in the one screen meant for checking the parse. */
const preview: PreviewRow[] = (parsed?.rows ?? []).map((row: SheetProductRow) => ({
productname: row.productname,
productsku: row.productsku,
categoryid: row.categoryid,
retailprice: row.retailprice,
productcost: row.productcost,
quantity: row.quantity,
}));
const preview: PreviewRow[] = (parsed?.rows ?? []).map((row: SheetProductRow) => {
/*
The category comes from the parser, which ran the ladder once; this
re-runs it only to recover WHICH RULE answered, so the column can say so.
The answer itself is the parser's own file, before a
single product is created — the same deterministic ladder the catalogue
team runs, so a product filed here and the same product in the global
catalogue land in the same place.
Shown rather than silently applied: the column used to print
`categoryid`, which the parser sets to 0 on every row, so it read "0" all
the way down and told nobody anything.
*/
const verdict = resolveCategory({
title: row.productname,
description: row.productdesc ?? '',
sheetValue: row.category ?? '',
packSize: [row.unitvalue, row.productunit].filter(Boolean).join(' '),
});
return {
productname: row.productname,
productsku: row.productsku,
category: verdict.category,
categoryRule: verdict.rule,
retailprice: row.retailprice,
productcost: row.productcost,
quantity: row.quantity,
};
});
// A new file is a new sheet, so the pager starts over.
const previewPaged = usePaged(preview, { resetKey: preview.length });