feat: enhance product analysis results with additional fields and update handling logic
This commit is contained in:
@@ -212,7 +212,33 @@ export async function insertProductAnalysisResults(
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?
|
||||
);
|
||||
)
|
||||
ON CONFLICT(asin) DO UPDATE SET
|
||||
run_id = excluded.run_id,
|
||||
name = excluded.name,
|
||||
brand = excluded.brand,
|
||||
category = excluded.category,
|
||||
unit_cost = excluded.unit_cost,
|
||||
current_price = excluded.current_price,
|
||||
avg_price_90d = excluded.avg_price_90d,
|
||||
avg_price_90d_sheet = excluded.avg_price_90d_sheet,
|
||||
selling_price_sheet = excluded.selling_price_sheet,
|
||||
sales_rank = excluded.sales_rank,
|
||||
sales_rank_avg_90d = excluded.sales_rank_avg_90d,
|
||||
seller_count = excluded.seller_count,
|
||||
monthly_sold = excluded.monthly_sold,
|
||||
rank_drops_30d = excluded.rank_drops_30d,
|
||||
rank_drops_90d = excluded.rank_drops_90d,
|
||||
fba_fee = excluded.fba_fee,
|
||||
fbm_fee = excluded.fbm_fee,
|
||||
referral_percent = excluded.referral_percent,
|
||||
can_sell = excluded.can_sell,
|
||||
sellability_status = excluded.sellability_status,
|
||||
sellability_reason = excluded.sellability_reason,
|
||||
verdict = excluded.verdict,
|
||||
confidence = excluded.confidence,
|
||||
reasoning = excluded.reasoning,
|
||||
fetched_at = excluded.fetched_at;
|
||||
`);
|
||||
|
||||
db.transaction((resultsBatch: AnalysisResult[]) => {
|
||||
|
||||
@@ -49,7 +49,7 @@ function createProductAnalysisResultsTable(database: Database): void {
|
||||
confidence REAL NOT NULL,
|
||||
reasoning TEXT,
|
||||
fetched_at TEXT NOT NULL,
|
||||
UNIQUE(run_id, asin),
|
||||
UNIQUE(asin),
|
||||
FOREIGN KEY (run_id) REFERENCES category_analysis_runs(id)
|
||||
);
|
||||
`);
|
||||
@@ -70,12 +70,38 @@ function ensureProductAnalysisResultsTable(database: Database): void {
|
||||
(col) => col.name === "asin" && col.pk === 1,
|
||||
);
|
||||
|
||||
if (!hasIdColumn || hasAsinPrimaryKey) {
|
||||
const indexList = database
|
||||
.query("PRAGMA index_list(product_analysis_results)")
|
||||
.all() as Array<{ name: string; unique: number }>;
|
||||
const hasUniqueAsinConstraint = indexList.some((idx) => {
|
||||
if (idx.unique !== 1) return false;
|
||||
const columns = database
|
||||
.query(`PRAGMA index_info(${JSON.stringify(idx.name)})`)
|
||||
.all() as Array<{ name: string }>;
|
||||
return columns.length === 1 && columns[0]?.name === "asin";
|
||||
});
|
||||
|
||||
if (!hasIdColumn || hasAsinPrimaryKey || !hasUniqueAsinConstraint) {
|
||||
database.run(
|
||||
"ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy",
|
||||
);
|
||||
createProductAnalysisResultsTable(database);
|
||||
database.run(`
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
asin, run_id, name, brand, category, unit_cost,
|
||||
current_price, avg_price_90d, avg_price_90d_sheet,
|
||||
selling_price_sheet, sales_rank, sales_rank_avg_90d,
|
||||
seller_count, monthly_sold, rank_drops_30d, rank_drops_90d,
|
||||
fba_fee, fbm_fee, referral_percent, can_sell,
|
||||
sellability_status, sellability_reason,
|
||||
verdict, confidence, reasoning, fetched_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY asin
|
||||
ORDER BY datetime(fetched_at) DESC, run_id DESC, id DESC
|
||||
) AS row_num
|
||||
FROM product_analysis_results_legacy
|
||||
)
|
||||
INSERT INTO product_analysis_results (
|
||||
asin, run_id, name, brand, category, unit_cost,
|
||||
current_price, avg_price_90d, avg_price_90d_sheet,
|
||||
@@ -93,7 +119,8 @@ function ensureProductAnalysisResultsTable(database: Database): void {
|
||||
fba_fee, fbm_fee, referral_percent, can_sell,
|
||||
sellability_status, sellability_reason,
|
||||
verdict, confidence, reasoning, fetched_at
|
||||
FROM product_analysis_results_legacy
|
||||
FROM ranked
|
||||
WHERE row_num = 1
|
||||
`);
|
||||
database.run("DROP TABLE product_analysis_results_legacy");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ type ProductListRecord = {
|
||||
seller_count: number | null;
|
||||
sales_rank: number | null;
|
||||
current_price: number | null;
|
||||
avg_price_90d: number | null;
|
||||
reasoning: string | null;
|
||||
fetched_at: string;
|
||||
};
|
||||
|
||||
@@ -286,6 +288,11 @@ function getProductList(filters: URLSearchParams) {
|
||||
"sales_rank",
|
||||
"current_price",
|
||||
"product_name",
|
||||
"brand",
|
||||
"category",
|
||||
"avg_price_90d",
|
||||
"confidence",
|
||||
"reasoning",
|
||||
"fetched_at",
|
||||
]);
|
||||
const orderBy = parseResultSort(
|
||||
@@ -309,6 +316,8 @@ function getProductList(filters: URLSearchParams) {
|
||||
sellers AS seller_count,
|
||||
sales_rank,
|
||||
current_price,
|
||||
avg_price_90d,
|
||||
reasoning,
|
||||
fetched_at
|
||||
FROM results
|
||||
UNION ALL
|
||||
@@ -326,6 +335,8 @@ function getProductList(filters: URLSearchParams) {
|
||||
seller_count,
|
||||
sales_rank,
|
||||
current_price,
|
||||
avg_price_90d,
|
||||
reasoning,
|
||||
fetched_at
|
||||
FROM product_analysis_results
|
||||
`;
|
||||
|
||||
@@ -91,6 +91,8 @@ type ProductListItem = {
|
||||
seller_count: number | null;
|
||||
sales_rank: number | null;
|
||||
current_price: number | null;
|
||||
avg_price_90d: number | null;
|
||||
reasoning: string | null;
|
||||
fetched_at: string;
|
||||
};
|
||||
|
||||
@@ -680,11 +682,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
<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>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "brand"))}>Brand</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "category"))}>Category</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "avg_price_90d"))}>Avg 90d</button></th>
|
||||
<th><button onClick={() => setSort(nextSort(sort, "confidence"))}>Confidence</button></th>
|
||||
<th className="reason-col"><button onClick={() => setSort(nextSort(sort, "reasoning"))}>Reasoning</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={7}>Loading...</td></tr>
|
||||
<tr><td colSpan={12}>Loading...</td></tr>
|
||||
) : items?.items.length ? (
|
||||
items.items.map((item) => (
|
||||
<tr key={`${item.processType}-${item.runId}-${item.asin}-${item.fetched_at}`}>
|
||||
@@ -694,11 +701,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
||||
<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>
|
||||
<td className="product-col" title={item.reasoning || undefined}>{item.product_name || "-"}</td>
|
||||
<td>{item.brand || "-"}</td>
|
||||
<td>{item.category || "-"}</td>
|
||||
<td>{formatCurrency(item.avg_price_90d)}</td>
|
||||
<td>{formatNumber(item.confidence)}</td>
|
||||
<td className="reason-col" title={item.reasoning || undefined}>{item.reasoning || "-"}</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr><td colSpan={7}>No products found</td></tr>
|
||||
<tr><td colSpan={12}>No products found</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user