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);