feat: enhance product listing with additional metrics and sorting options
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
id,name
|
||||
19419898011,Amazon Explore
|
||||
14297978011,Online Learning
|
||||
229534,Software
|
||||
283155,Books
|
||||
16310101,Grocery Gourmet Food
|
||||
599858,Magazine Subscriptions
|
||||
|
@@ -27,6 +27,10 @@ type ProductListRecord = {
|
||||
verdict: "FBA" | "FBM" | "SKIP";
|
||||
confidence: number | null;
|
||||
sellability_status: string | null;
|
||||
monthly_sold: number | null;
|
||||
seller_count: number | null;
|
||||
sales_rank: number | null;
|
||||
current_price: number | null;
|
||||
fetched_at: string;
|
||||
};
|
||||
|
||||
@@ -274,6 +278,21 @@ function getProductList(filters: URLSearchParams) {
|
||||
}
|
||||
|
||||
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 = `
|
||||
SELECT
|
||||
@@ -286,6 +305,10 @@ function getProductList(filters: URLSearchParams) {
|
||||
verdict,
|
||||
confidence,
|
||||
sellability_status,
|
||||
monthly_sold,
|
||||
sellers AS seller_count,
|
||||
sales_rank,
|
||||
current_price,
|
||||
fetched_at
|
||||
FROM results
|
||||
UNION ALL
|
||||
@@ -299,6 +322,10 @@ function getProductList(filters: URLSearchParams) {
|
||||
verdict,
|
||||
confidence,
|
||||
sellability_status,
|
||||
monthly_sold,
|
||||
seller_count,
|
||||
sales_rank,
|
||||
current_price,
|
||||
fetched_at
|
||||
FROM product_analysis_results
|
||||
`;
|
||||
@@ -308,7 +335,7 @@ function getProductList(filters: URLSearchParams) {
|
||||
.get(...params) as { total: number };
|
||||
|
||||
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[];
|
||||
|
||||
return {
|
||||
|
||||
@@ -87,6 +87,10 @@ type ProductListItem = {
|
||||
verdict: "FBA" | "FBM" | "SKIP";
|
||||
confidence: number | null;
|
||||
sellability_status: string | null;
|
||||
monthly_sold: number | null;
|
||||
seller_count: number | null;
|
||||
sales_rank: number | null;
|
||||
current_price: number | null;
|
||||
fetched_at: string;
|
||||
};
|
||||
|
||||
@@ -615,6 +619,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
const [activeVerdict, setActiveVerdict] = useState<VerdictFilter>(verdict);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
const [sort, setSort] = useState<SortState>({ field: "monthly_sold", direction: "DESC" });
|
||||
|
||||
useEffect(() => {
|
||||
setActiveVerdict(verdict);
|
||||
@@ -626,6 +631,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
||||
params.set("sort", buildSortValue(sort));
|
||||
if (search) params.set("q", search);
|
||||
if (activeVerdict) params.set("verdict", activeVerdict);
|
||||
const res = await fetch(`/api/products?${params.toString()}`);
|
||||
@@ -640,7 +646,7 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [search, activeVerdict, page, pageSize]);
|
||||
}, [search, activeVerdict, page, pageSize, sort]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
@@ -667,36 +673,32 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ASIN</th>
|
||||
<th>Verdict</th>
|
||||
<th>Product</th>
|
||||
<th>Brand</th>
|
||||
<th>Category</th>
|
||||
<th>Confidence</th>
|
||||
<th>Process</th>
|
||||
<th>Run ID</th>
|
||||
<th>Fetched</th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "asin"))}>ASIN</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "verdict"))}>Verdict</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "monthly_sold"))}>Monthly Sold</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "seller_count"))}>Sellers</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "sales_rank"))}>Sales Rank</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "current_price"))}>Current Price</button></th>
|
||||
<th className="product-col"><button onClick={() => setSort(nextSort(sort, "product_name"))}>Product</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={9}>Loading...</td></tr>
|
||||
<tr><td colSpan={7}>Loading...</td></tr>
|
||||
) : items?.items.length ? (
|
||||
items.items.map((item) => (
|
||||
<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><span className={verdictBadgeClass(item.verdict)}>{item.verdict}</span></td>
|
||||
<td>{item.product_name || "-"}</td>
|
||||
<td>{item.brand || "-"}</td>
|
||||
<td>{item.category || "-"}</td>
|
||||
<td>{formatNumber(item.confidence)}</td>
|
||||
<td>{item.processType}</td>
|
||||
<td>{item.runId}</td>
|
||||
<td>{formatDate(item.fetched_at)}</td>
|
||||
<td>{formatNumber(item.monthly_sold)}</td>
|
||||
<td>{formatNumber(item.seller_count)}</td>
|
||||
<td>{formatNumber(item.sales_rank)}</td>
|
||||
<td>{formatCurrency(item.current_price)}</td>
|
||||
<td className="product-col" title={item.product_name || undefined}>{item.product_name || "-"}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr><td colSpan={9}>No products found</td></tr>
|
||||
<tr><td colSpan={7}>No products found</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -84,6 +84,13 @@ td {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.product-col {
|
||||
min-width: 300px;
|
||||
max-width: 440px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fafafb;
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user