feat: enhance product listing with additional metrics and sorting options
This commit is contained in:
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user