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[]) => {
|
db.transaction((resultsBatch: AnalysisResult[]) => {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function createProductAnalysisResultsTable(database: Database): void {
|
|||||||
confidence REAL NOT NULL,
|
confidence REAL NOT NULL,
|
||||||
reasoning TEXT,
|
reasoning TEXT,
|
||||||
fetched_at TEXT NOT NULL,
|
fetched_at TEXT NOT NULL,
|
||||||
UNIQUE(run_id, asin),
|
UNIQUE(asin),
|
||||||
FOREIGN KEY (run_id) REFERENCES category_analysis_runs(id)
|
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,
|
(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(
|
database.run(
|
||||||
"ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy",
|
"ALTER TABLE product_analysis_results RENAME TO product_analysis_results_legacy",
|
||||||
);
|
);
|
||||||
createProductAnalysisResultsTable(database);
|
createProductAnalysisResultsTable(database);
|
||||||
database.run(`
|
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 (
|
INSERT INTO product_analysis_results (
|
||||||
asin, run_id, name, brand, category, unit_cost,
|
asin, run_id, name, brand, category, unit_cost,
|
||||||
current_price, avg_price_90d, avg_price_90d_sheet,
|
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,
|
fba_fee, fbm_fee, referral_percent, can_sell,
|
||||||
sellability_status, sellability_reason,
|
sellability_status, sellability_reason,
|
||||||
verdict, confidence, reasoning, fetched_at
|
verdict, confidence, reasoning, fetched_at
|
||||||
FROM product_analysis_results_legacy
|
FROM ranked
|
||||||
|
WHERE row_num = 1
|
||||||
`);
|
`);
|
||||||
database.run("DROP TABLE product_analysis_results_legacy");
|
database.run("DROP TABLE product_analysis_results_legacy");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ type ProductListRecord = {
|
|||||||
seller_count: number | null;
|
seller_count: number | null;
|
||||||
sales_rank: number | null;
|
sales_rank: number | null;
|
||||||
current_price: number | null;
|
current_price: number | null;
|
||||||
|
avg_price_90d: number | null;
|
||||||
|
reasoning: string | null;
|
||||||
fetched_at: string;
|
fetched_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,6 +288,11 @@ function getProductList(filters: URLSearchParams) {
|
|||||||
"sales_rank",
|
"sales_rank",
|
||||||
"current_price",
|
"current_price",
|
||||||
"product_name",
|
"product_name",
|
||||||
|
"brand",
|
||||||
|
"category",
|
||||||
|
"avg_price_90d",
|
||||||
|
"confidence",
|
||||||
|
"reasoning",
|
||||||
"fetched_at",
|
"fetched_at",
|
||||||
]);
|
]);
|
||||||
const orderBy = parseResultSort(
|
const orderBy = parseResultSort(
|
||||||
@@ -309,6 +316,8 @@ function getProductList(filters: URLSearchParams) {
|
|||||||
sellers AS seller_count,
|
sellers AS seller_count,
|
||||||
sales_rank,
|
sales_rank,
|
||||||
current_price,
|
current_price,
|
||||||
|
avg_price_90d,
|
||||||
|
reasoning,
|
||||||
fetched_at
|
fetched_at
|
||||||
FROM results
|
FROM results
|
||||||
UNION ALL
|
UNION ALL
|
||||||
@@ -326,6 +335,8 @@ function getProductList(filters: URLSearchParams) {
|
|||||||
seller_count,
|
seller_count,
|
||||||
sales_rank,
|
sales_rank,
|
||||||
current_price,
|
current_price,
|
||||||
|
avg_price_90d,
|
||||||
|
reasoning,
|
||||||
fetched_at
|
fetched_at
|
||||||
FROM product_analysis_results
|
FROM product_analysis_results
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ type ProductListItem = {
|
|||||||
seller_count: number | null;
|
seller_count: number | null;
|
||||||
sales_rank: number | null;
|
sales_rank: number | null;
|
||||||
current_price: number | null;
|
current_price: number | null;
|
||||||
|
avg_price_90d: number | null;
|
||||||
|
reasoning: string | null;
|
||||||
fetched_at: string;
|
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, "sales_rank"))}>Sales Rank</button></th>
|
||||||
<th><button onClick={() => setSort(nextSort(sort, "current_price"))}>Current Price</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 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={7}>Loading...</td></tr>
|
<tr><td colSpan={12}>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}`}>
|
||||||
@@ -694,11 +701,16 @@ function ProductList({ verdict, onBack }: { verdict: VerdictFilter; onBack: () =
|
|||||||
<td>{formatNumber(item.seller_count)}</td>
|
<td>{formatNumber(item.seller_count)}</td>
|
||||||
<td>{formatNumber(item.sales_rank)}</td>
|
<td>{formatNumber(item.sales_rank)}</td>
|
||||||
<td>{formatCurrency(item.current_price)}</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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr><td colSpan={7}>No products found</td></tr>
|
<tr><td colSpan={12}>No products found</td></tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user