feat: enhance product listing with additional metrics and sorting options

This commit is contained in:
Victor Noguera
2026-04-13 03:04:28 -04:00
parent 937fe5da40
commit 299ad7a1a6
4 changed files with 60 additions and 21 deletions

View File

@@ -1,4 +1,7 @@
id,name id,name
19419898011,Amazon Explore
14297978011,Online Learning
229534,Software 229534,Software
283155,Books 283155,Books
16310101,Grocery Gourmet Food 16310101,Grocery Gourmet Food
599858,Magazine Subscriptions
1 id name
2 19419898011 Amazon Explore
3 14297978011 Online Learning
4 229534 Software
5 283155 Books
6 16310101 Grocery Gourmet Food
7 599858 Magazine Subscriptions

View File

@@ -27,6 +27,10 @@ type ProductListRecord = {
verdict: "FBA" | "FBM" | "SKIP"; verdict: "FBA" | "FBM" | "SKIP";
confidence: number | null; confidence: number | null;
sellability_status: string | null; sellability_status: string | null;
monthly_sold: number | null;
seller_count: number | null;
sales_rank: number | null;
current_price: number | null;
fetched_at: string; fetched_at: string;
}; };
@@ -274,6 +278,21 @@ function getProductList(filters: URLSearchParams) {
} }
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const allowedSort = new Set([
"asin",
"verdict",
"monthly_sold",
"seller_count",
"sales_rank",
"current_price",
"product_name",
"fetched_at",
]);
const orderBy = parseResultSort(
filters.get("sort"),
allowedSort,
"CAST(COALESCE(monthly_sold, 0) AS INTEGER) DESC, fetched_at DESC",
);
const baseUnion = ` const baseUnion = `
SELECT SELECT
@@ -286,6 +305,10 @@ function getProductList(filters: URLSearchParams) {
verdict, verdict,
confidence, confidence,
sellability_status, sellability_status,
monthly_sold,
sellers AS seller_count,
sales_rank,
current_price,
fetched_at fetched_at
FROM results FROM results
UNION ALL UNION ALL
@@ -299,6 +322,10 @@ function getProductList(filters: URLSearchParams) {
verdict, verdict,
confidence, confidence,
sellability_status, sellability_status,
monthly_sold,
seller_count,
sales_rank,
current_price,
fetched_at fetched_at
FROM product_analysis_results FROM product_analysis_results
`; `;
@@ -308,7 +335,7 @@ function getProductList(filters: URLSearchParams) {
.get(...params) as { total: number }; .get(...params) as { total: number };
const items = db const items = db
.query(`SELECT * FROM (${baseUnion}) all_products ${where} ORDER BY fetched_at DESC LIMIT ? OFFSET ?`) .query(`SELECT * FROM (${baseUnion}) all_products ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`)
.all(...params, pageSize, offset) as ProductListRecord[]; .all(...params, pageSize, offset) as ProductListRecord[];
return { return {

View File

@@ -87,6 +87,10 @@ type ProductListItem = {
verdict: "FBA" | "FBM" | "SKIP"; verdict: "FBA" | "FBM" | "SKIP";
confidence: number | null; confidence: number | null;
sellability_status: string | null; sellability_status: string | null;
monthly_sold: number | null;
seller_count: number | null;
sales_rank: number | null;
current_price: number | null;
fetched_at: string; fetched_at: string;
}; };
@@ -615,6 +619,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
const [activeVerdict, setActiveVerdict] = useState<VerdictFilter>(verdict); const [activeVerdict, setActiveVerdict] = useState<VerdictFilter>(verdict);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25); const [pageSize, setPageSize] = useState(25);
const [sort, setSort] = useState<SortState>({ field: "monthly_sold", direction: "DESC" });
useEffect(() => { useEffect(() => {
setActiveVerdict(verdict); setActiveVerdict(verdict);
@@ -626,6 +631,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
async function load() { async function load() {
setLoading(true); setLoading(true);
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
params.set("sort", buildSortValue(sort));
if (search) params.set("q", search); if (search) params.set("q", search);
if (activeVerdict) params.set("verdict", activeVerdict); if (activeVerdict) params.set("verdict", activeVerdict);
const res = await fetch(`/api/products?${params.toString()}`); const res = await fetch(`/api/products?${params.toString()}`);
@@ -640,7 +646,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [search, activeVerdict, page, pageSize]); }, [search, activeVerdict, page, pageSize, sort]);
return ( return (
<div className="page"> <div className="page">
@@ -667,36 +673,32 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ASIN</th> <th><button onClick={() => setSort(nextSort(sort, "asin"))}>ASIN</button></th>
<th>Verdict</th> <th><button onClick={() => setSort(nextSort(sort, "verdict"))}>Verdict</button></th>
<th>Product</th> <th><button onClick={() => setSort(nextSort(sort, "monthly_sold"))}>Monthly Sold</button></th>
<th>Brand</th> <th><button onClick={() => setSort(nextSort(sort, "seller_count"))}>Sellers</button></th>
<th>Category</th> <th><button onClick={() => setSort(nextSort(sort, "sales_rank"))}>Sales Rank</button></th>
<th>Confidence</th> <th><button onClick={() => setSort(nextSort(sort, "current_price"))}>Current Price</button></th>
<th>Process</th> <th className="product-col"><button onClick={() => setSort(nextSort(sort, "product_name"))}>Product</button></th>
<th>Run ID</th>
<th>Fetched</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{loading ? ( {loading ? (
<tr><td colSpan={9}>Loading...</td></tr> <tr><td colSpan={7}>Loading...</td></tr>
) : items?.items.length ? ( ) : items?.items.length ? (
items.items.map((item) => ( items.items.map((item) => (
<tr key={`${item.processType}-${item.runId}-${item.asin}-${item.fetched_at}`}> <tr key={`${item.processType}-${item.runId}-${item.asin}-${item.fetched_at}`}>
<td><a href={`http://amazon.com/dp/${item.asin}`} target="_blank" rel="noreferrer">{item.asin}</a></td> <td><a href={`http://amazon.com/dp/${item.asin}`} target="_blank" rel="noreferrer">{item.asin}</a></td>
<td><span className={verdictBadgeClass(item.verdict)}>{item.verdict}</span></td> <td><span className={verdictBadgeClass(item.verdict)}>{item.verdict}</span></td>
<td>{item.product_name || "-"}</td> <td>{formatNumber(item.monthly_sold)}</td>
<td>{item.brand || "-"}</td> <td>{formatNumber(item.seller_count)}</td>
<td>{item.category || "-"}</td> <td>{formatNumber(item.sales_rank)}</td>
<td>{formatNumber(item.confidence)}</td> <td>{formatCurrency(item.current_price)}</td>
<td>{item.processType}</td> <td className="product-col" title={item.product_name || undefined}>{item.product_name || "-"}</td>
<td>{item.runId}</td>
<td>{formatDate(item.fetched_at)}</td>
</tr> </tr>
)) ))
) : ( ) : (
<tr><td colSpan={9}>No products found</td></tr> <tr><td colSpan={7}>No products found</td></tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@@ -84,6 +84,13 @@ td {
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.product-col {
min-width: 300px;
max-width: 440px;
white-space: normal;
overflow-wrap: anywhere;
}
th { th {
background: #fafafb; background: #fafafb;
font-weight: 600; font-weight: 600;