feat: add XLSX export functionality for Stalker products and enhance UI for export link
This commit is contained in:
132
src/server.ts
132
src/server.ts
@@ -1,5 +1,6 @@
|
||||
import index from "./web/index.html";
|
||||
import path from "node:path";
|
||||
import * as XLSX from "xlsx";
|
||||
import { getDb, initDb } from "./database.ts";
|
||||
import {
|
||||
fetchKeepaDataBatch,
|
||||
@@ -123,6 +124,16 @@ function csv(text: string, filename: string): Response {
|
||||
});
|
||||
}
|
||||
|
||||
function xlsx(buffer: ArrayBuffer, filename: string): Response {
|
||||
return new Response(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"content-disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function parseIntParam(value: string | null, fallback: number): number {
|
||||
if (!value) return fallback;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
@@ -1025,6 +1036,123 @@ function getStalkerProducts(filters: URLSearchParams) {
|
||||
};
|
||||
}
|
||||
|
||||
function getStalkerProductsForExport(filters: URLSearchParams): StalkerProductRecord[] {
|
||||
const { where, params } = parseStalkerProductFilters(filters);
|
||||
const orderBy = parseStalkerProductSort(filters.get("sort"));
|
||||
|
||||
return db
|
||||
.query(
|
||||
`SELECT * FROM (
|
||||
SELECT
|
||||
r.id AS runId,
|
||||
r.started_at,
|
||||
s.seller_id,
|
||||
s.seller_name,
|
||||
s.rating,
|
||||
s.rating_count,
|
||||
inv.asin,
|
||||
inv.can_sell,
|
||||
inv.sellability_status,
|
||||
inv.sellability_reason,
|
||||
inv.product_title,
|
||||
inv.brand,
|
||||
inv.category_tree,
|
||||
inv.current_price,
|
||||
inv.avg_price_90d,
|
||||
inv.sales_rank,
|
||||
inv.monthly_sold,
|
||||
inv.seller_count,
|
||||
inv.amazon_is_seller,
|
||||
analysis.verdict,
|
||||
analysis.confidence,
|
||||
analysis.reasoning,
|
||||
inv.last_seen_at
|
||||
FROM stalker_seller_inventory inv
|
||||
JOIN stalker_runs r ON r.id = inv.run_id
|
||||
JOIN stalker_sellers s ON s.seller_id = inv.seller_id
|
||||
LEFT JOIN product_analysis_results analysis ON analysis.asin = inv.asin
|
||||
${where}
|
||||
) stalker_products
|
||||
ORDER BY ${orderBy}`,
|
||||
)
|
||||
.all(...params) as StalkerProductRecord[];
|
||||
}
|
||||
|
||||
function parseCategoryTreeForExport(value: string | null): string {
|
||||
if (!value) return "";
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((item) => typeof item === "string").join(" > ")
|
||||
: "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function exportStalkerProductsXlsx(filters: URLSearchParams): Response {
|
||||
const rows = getStalkerProductsForExport(filters);
|
||||
const data = rows.map((row) => ({
|
||||
ASIN: row.asin,
|
||||
"Amazon URL": `https://amazon.com/dp/${row.asin}`,
|
||||
Product: row.product_title ?? "",
|
||||
Brand: row.brand ?? "",
|
||||
Category: parseCategoryTreeForExport(row.category_tree),
|
||||
"Monthly Sold": row.monthly_sold ?? null,
|
||||
Sellers: row.seller_count ?? null,
|
||||
"Amazon Seller": row.amazon_is_seller == null ? "" : row.amazon_is_seller === 1 ? "Yes" : "No",
|
||||
"Sales Rank": row.sales_rank ?? null,
|
||||
"Current Price": row.current_price ?? null,
|
||||
"Avg 90d": row.avg_price_90d ?? null,
|
||||
Verdict: row.verdict ?? "",
|
||||
Confidence: row.confidence ?? null,
|
||||
Reasoning: row.reasoning ?? "",
|
||||
"Seller ID": row.seller_id,
|
||||
Seller: row.seller_name ?? "",
|
||||
"Seller Rating": row.rating ?? null,
|
||||
"Seller Rating Count": row.rating_count ?? null,
|
||||
"Sellability Status": row.sellability_status,
|
||||
"Sellability Reason": row.sellability_reason ?? "",
|
||||
"Run ID": row.runId,
|
||||
"Last Seen": row.last_seen_at,
|
||||
}));
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||
worksheet["!cols"] = [
|
||||
{ wch: 12 },
|
||||
{ wch: 32 },
|
||||
{ wch: 48 },
|
||||
{ wch: 20 },
|
||||
{ wch: 34 },
|
||||
{ wch: 14 },
|
||||
{ wch: 10 },
|
||||
{ wch: 14 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 12 },
|
||||
{ wch: 10 },
|
||||
{ wch: 12 },
|
||||
{ wch: 60 },
|
||||
{ wch: 18 },
|
||||
{ wch: 24 },
|
||||
{ wch: 12 },
|
||||
{ wch: 20 },
|
||||
{ wch: 18 },
|
||||
{ wch: 40 },
|
||||
{ wch: 10 },
|
||||
{ wch: 24 },
|
||||
];
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, "Sellable Products");
|
||||
|
||||
const buffer = XLSX.write(workbook, {
|
||||
type: "array",
|
||||
bookType: "xlsx",
|
||||
}) as ArrayBuffer;
|
||||
|
||||
return xlsx(buffer, "stalker-sellable-products.xlsx");
|
||||
}
|
||||
|
||||
function purgeStalkerData() {
|
||||
const counts = {
|
||||
inventory: (db.query("SELECT COUNT(*) AS count FROM stalker_seller_inventory").get() as { count: number }).count,
|
||||
@@ -1694,6 +1822,10 @@ const server = Bun.serve({
|
||||
const url = new URL(req.url);
|
||||
return json(getStalkerProducts(url.searchParams));
|
||||
},
|
||||
"/api/stalker/products/export.xlsx": (req) => {
|
||||
const url = new URL(req.url);
|
||||
return exportStalkerProductsXlsx(url.searchParams);
|
||||
},
|
||||
"/api/stalker/purge": (req) => {
|
||||
if (req.method !== "DELETE" && req.method !== "POST") {
|
||||
return json({ error: "Method not allowed" }, 405);
|
||||
|
||||
Reference in New Issue
Block a user