frontend changes on UI

This commit is contained in:
sriram
2026-10-06 12:02:47 +05:30
parent b58b291a0c
commit 3c4d9a7c8a

View File

@@ -33,6 +33,12 @@ import { api } from '../api/client';
* the brand and states a pack size. That search takes minutes, so it runs as a
* job: Discover starts it, this panel polls it, then asks for the preview.
*
* A PRODUCT NAME WORKS TOO. "Lizol" resolves to Reckitt Benckiser and searches
* only Lizol listings; "Lizol Floor Cleaner" narrows to that line. The result
* is shown grouped - product line, then each variant (Citrus, Floral...), then
* the pack sizes real listings name - above the row-by-row table, and a size
* chip ticks or unticks the rows behind it.
*
* There is no progress UI here on purpose. Ingest returns a BatchOut, and
* AdminPage hands it to BatchCatalogPanel through the `adoptBatch` prop it
* already has - which draws the eleven stages, and owns Resume and Cancel.
@@ -72,6 +78,66 @@ function evidenceBadge(product) {
return null;
}
// "Lizol" typed, "reckitt benckiser" resolved: say so, and say where rows go.
function resolvedNote(result) {
const typed = (result.brand || '').trim().toLowerCase();
const parent = (result.parent_brand || '').trim().toLowerCase();
if (!parent || typed === parent) return null;
const maker = result.parent_brand.split(' ').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
return `"${result.brand}" is a product of ${maker} - only its own listings were searched.`;
}
// Product line -> variant -> pack sizes. A size chip stands for the preview
// rows behind it (`rows`), and clicking it ticks or unticks all of them.
function Families({ families, selected, onToggleRows }) {
if (!families?.length) return null;
return (
<div className="space-y-3">
{families.map((family) => (
<div
key={family.product_line}
className="rounded-2xl border border-ink-900/10 bg-white px-4 py-3"
>
<div className="text-sm font-bold text-ink-950">{family.product_line}</div>
<div className="mt-2 space-y-1.5">
{family.variants.map((variant) => (
<div key={variant.variant || '-'} className="flex flex-wrap items-center gap-1.5 text-xs">
<span className="min-w-24 font-semibold text-slate-700">
{variant.variant || 'Regular'}
</span>
{variant.sizes.map((size) => {
const rows = size.rows || [];
const on = rows.length > 0 && rows.every((i) => selected.has(i));
const shops = size.retailer_count
? `${size.retailer_count} shop${size.retailer_count === 1 ? '' : 's'}`
: '';
return (
<button
key={size.size}
type="button"
onClick={() => onToggleRows(rows, !on)}
title={[shops, size.price_range, (size.providers || []).join(', ')]
.filter(Boolean).join(' - ')}
className={`rounded-full border px-2 py-0.5 text-[11px] font-semibold cursor-pointer ${
on
? 'border-amber-500 bg-amber-500/15 text-ink-950'
: 'border-ink-900/10 bg-paper-50 text-slate-500'
}`}
>
{size.size}
{size.price_range ? ` · ${size.price_range}` : ''}
</button>
);
})}
</div>
))}
</div>
</div>
))}
</div>
);
}
function Toggle({ checked, onChange, label, hint }) {
return (
<label className="flex items-start gap-2 text-xs text-slate-600 cursor-pointer">
@@ -194,6 +260,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
fssai_license: p.fssai_license,
barcode: p.barcode,
image_url: p.image_url,
product_line: p.product_line,
variant: p.variant,
// Web-found rows only; recorded as provenance after the batch.
...(p.listings?.length
? { listings: p.listings, price_range: p.price_range, retailer_count: p.retailer_count }
@@ -222,6 +290,14 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
});
};
const toggleRows = (rows, on) => {
setSelected((prev) => {
const next = new Set(prev);
rows.forEach((i) => (on ? next.add(i) : next.delete(i)));
return next;
});
};
const allSelected = products.length > 0 && selected.size === products.length;
return (
@@ -233,7 +309,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
<h2 className="text-sm font-bold text-ink-950">Brand Discovery</h2>
</div>
<p className="text-xs text-slate-600 max-w-3xl">
Enter any brand - one already in the catalog or a new one. Products
Enter any brand - one already in the catalog or a new one - or a
product such as Lizol, which is searched under its maker. Products
come from Open Food Facts, which supplies real names, pack sizes and
barcodes, and from the brand's own online shop where one is
registered. Nothing is written until you review the list and press
@@ -247,7 +324,7 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
value={brand}
onChange={(e) => setBrand(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && discover()}
placeholder="Britannia"
placeholder="Britannia, or a product like Lizol"
className="w-full rounded-xl border border-ink-900/10 bg-white py-2 pl-9 pr-3 text-sm text-ink-950 outline-none focus:border-amber-500"
/>
</div>
@@ -317,6 +394,13 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
)}
</div>
{resolvedNote(result) && (
<div className="flex items-start gap-2 rounded-xl border border-sky-500/30 bg-sky-500/10 px-4 py-3 text-xs text-sky-800">
<Search className="mt-0.5 h-4 w-4 shrink-0" />
<span>{resolvedNote(result)}</span>
</div>
)}
{(result.warnings || []).map((warning) => (
<div
key={warning}
@@ -358,6 +442,8 @@ export function BrandDiscoveryPanel({ onBatchStarted }) {
</button>
</div>
<Families families={result.families} selected={selected} onToggleRows={toggleRows} />
<div className="overflow-x-auto rounded-2xl border border-ink-900/10 bg-white">
<table className="w-full text-xs">
<thead className="border-b border-ink-900/10 bg-paper-50 text-left text-slate-500">