This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
amazon-po-parser/monitor.mjs

164 lines
7.2 KiB
JavaScript
Raw Permalink Normal View History

import { readFileSync, watchFile, existsSync } from "fs";
const OUTPUT_FILE = "output/scraped-pos.json";
const PROGRESS_FILE = "output/progress.json";
const POLL_MS = 5000;
let lastCount = 0;
function classify(description) {
if (!description) return null;
const d = description.toLowerCase();
if (!d.includes("plumbing")) return null;
if (d.includes("plumbing pm") || d.includes("pm and corrective")) return "PM";
return "Reactive";
}
function extractSite(record) {
if (record.site_code) return record.site_code;
for (const li of record.line_items || []) {
if (!li.description) continue;
const m = li.description.match(/^([A-Z]{1,4}[0-9]{1,2}|[A-Z]{4,5})\b/);
if (m) return m[1];
}
if (record.ship_to_raw) {
const m = record.ship_to_raw.match(/\(([A-Z0-9]{3,5})\)/);
if (m) return m[1];
}
return "Unknown";
}
function analyze() {
if (!existsSync(OUTPUT_FILE)) {
console.log("Waiting for scrape output...");
return;
}
let data;
try {
data = JSON.parse(readFileSync(OUTPUT_FILE, "utf-8"));
} catch {
return;
}
if (data.length === lastCount) return;
lastCount = data.length;
let progress = {};
if (existsSync(PROGRESS_FILE)) {
try {
progress = JSON.parse(readFileSync(PROGRESS_FILE, "utf-8"));
} catch {}
}
const sites = {};
let pmTotal = 0;
let reactiveTotal = 0;
let pmCount = 0;
let reactiveCount = 0;
let nonPlumbing = 0;
const plumbingPOs = [];
for (const record of data) {
let isPlumbing = false;
let category = null;
for (const li of record.line_items || []) {
const cat = classify(li.description);
if (cat) {
isPlumbing = true;
category = cat;
break;
}
}
if (!isPlumbing) {
nonPlumbing++;
continue;
}
const site = extractSite(record);
const amount = parseFloat(
(record.line_items[0]?.total || "0").replace(/,/g, "")
);
if (!sites[site]) sites[site] = { pm: 0, reactive: 0, pmCount: 0, reactiveCount: 0 };
if (category === "PM") {
sites[site].pm += amount;
sites[site].pmCount++;
pmTotal += amount;
pmCount++;
} else {
sites[site].reactive += amount;
sites[site].reactiveCount++;
reactiveTotal += amount;
reactiveCount++;
}
plumbingPOs.push({ po: record.po_number, site, category, amount });
}
// Clear screen and print dashboard
console.clear();
console.log("╔══════════════════════════════════════════════════════════════╗");
console.log("║ COUPA PO SCRAPER — LIVE PLUMBING MONITOR ║");
console.log("╚══════════════════════════════════════════════════════════════╝");
console.log();
console.log(` Scraped: ${data.length} / 4,192 POs (${Object.keys(progress).length} completed)`);
console.log(` Plumbing: ${pmCount + reactiveCount} POs | Non-plumbing: ${nonPlumbing}`);
console.log();
console.log(" ┌──────────────┬──────────────────┬──────────────────┬──────────────────┐");
console.log(" │ Category │ PO Count │ Spend │ Avg per PO │");
console.log(" ├──────────────┼──────────────────┼──────────────────┼──────────────────┤");
console.log(
` │ PM │ ${String(pmCount).padStart(16)} │ $${pmTotal.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(pmCount ? pmTotal / pmCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │`
);
console.log(
` │ Reactive │ ${String(reactiveCount).padStart(16)} │ $${reactiveTotal.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(reactiveCount ? reactiveTotal / reactiveCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │`
);
const total = pmTotal + reactiveTotal;
const totalCount = pmCount + reactiveCount;
console.log(" ├──────────────┼──────────────────┼──────────────────┼──────────────────┤");
console.log(
` │ TOTAL │ ${String(totalCount).padStart(16)} │ $${total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${(totalCount ? total / totalCount : 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │`
);
console.log(" └──────────────┴──────────────────┴──────────────────┴──────────────────┘");
// Top sites
const sortedSites = Object.entries(sites)
.map(([site, d]) => ({ site, total: d.pm + d.reactive, ...d }))
.sort((a, b) => b.total - a.total);
if (sortedSites.length > 0) {
console.log();
console.log(" Top 15 Sites:");
console.log(" ┌──────────┬──────────────────┬──────────────────┬──────────────────┐");
console.log(" │ Site │ PM Spend │ Reactive Spend │ Total │");
console.log(" ├──────────┼──────────────────┼──────────────────┼──────────────────┤");
for (const s of sortedSites.slice(0, 15)) {
console.log(
` │ ${s.site.padEnd(8)} │ $${s.pm.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${s.reactive.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │ $${s.total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).padStart(15)} │`
);
}
console.log(" └──────────┴──────────────────┴──────────────────┴──────────────────┘");
}
// Last 5 plumbing POs scraped
if (plumbingPOs.length > 0) {
console.log();
console.log(" Last 5 plumbing POs:");
for (const p of plumbingPOs.slice(-5)) {
console.log(` ${p.po} ${p.site.padEnd(8)} ${p.category.padEnd(10)} $${p.amount.toLocaleString()}`);
}
}
console.log();
console.log(` Last updated: ${new Date().toLocaleTimeString()}`);
console.log(" Press Ctrl+C to stop monitoring.");
}
// Run immediately, then poll
analyze();
setInterval(analyze, POLL_MS);