Guest User

Costco Receipts Analyzer

a guest
Nov 23rd, 2025
5,915
3
Never
8
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 22.78 KB | None | 3 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <title>Costco Receipt Dashboard</title>
  6. <meta name="viewport" content="width=device-width, initial-scale=1" />
  7.  
  8. <!-- Chart.js for the Monthly Spending Trend -->
  9. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  10.  
  11. <style>
  12. :root {
  13. --bg: #f5f7fb;
  14. --card-bg: #ffffff;
  15. --accent: #2563eb;
  16. --accent-soft: #e0edff;
  17. --text-main: #111827;
  18. --text-muted: #6b7280;
  19. --border: #e5e7eb;
  20. --danger: #dc2626;
  21. --success: #16a34a;
  22. --shadow-soft: 0 10px 25px rgba(15, 23, 42, 0.05);
  23. --radius-lg: 18px;
  24. }
  25.  
  26. * {
  27. box-sizing: border-box;
  28. }
  29.  
  30. body {
  31. margin: 0;
  32. font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text",
  33. "Segoe UI", sans-serif;
  34. background: var(--bg);
  35. color: var(--text-main);
  36. }
  37.  
  38. .page {
  39. max-width: 1200px;
  40. margin: 0 auto;
  41. padding: 24px 16px 64px;
  42. }
  43.  
  44. header {
  45. display: flex;
  46. flex-wrap: wrap;
  47. align-items: center;
  48. justify-content: space-between;
  49. gap: 16px;
  50. margin-bottom: 20px;
  51. }
  52.  
  53. header h1 {
  54. margin: 0;
  55. font-size: 1.8rem;
  56. display: flex;
  57. align-items: center;
  58. gap: 8px;
  59. }
  60.  
  61. header h1 span.icon {
  62. font-size: 1.6rem;
  63. }
  64.  
  65. header p {
  66. margin: 4px 0 0;
  67. color: var(--text-muted);
  68. font-size: 0.95rem;
  69. }
  70.  
  71. .uploader {
  72. padding: 10px 14px;
  73. background: var(--card-bg);
  74. border-radius: 999px;
  75. border: 1px solid var(--border);
  76. box-shadow: var(--shadow-soft);
  77. display: flex;
  78. align-items: center;
  79. gap: 10px;
  80. font-size: 0.9rem;
  81. }
  82.  
  83. .uploader label {
  84. font-weight: 600;
  85. }
  86.  
  87. .uploader input[type="file"] {
  88. border: none;
  89. font-size: 0.9rem;
  90. }
  91.  
  92. .status {
  93. font-size: 0.85rem;
  94. color: var(--text-muted);
  95. }
  96.  
  97. .status.error {
  98. color: var(--danger);
  99. }
  100.  
  101. .summary-grid {
  102. display: grid;
  103. grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  104. gap: 14px;
  105. margin-bottom: 24px;
  106. }
  107.  
  108. .summary-card {
  109. background: var(--card-bg);
  110. border-radius: var(--radius-lg);
  111. padding: 14px 16px;
  112. box-shadow: var(--shadow-soft);
  113. border: 1px solid var(--border);
  114. }
  115.  
  116. .summary-card .label {
  117. font-size: 0.8rem;
  118. text-transform: uppercase;
  119. letter-spacing: 0.06em;
  120. color: var(--text-muted);
  121. }
  122.  
  123. .summary-card .value {
  124. margin-top: 4px;
  125. font-size: 1.35rem;
  126. font-weight: 700;
  127. }
  128.  
  129. .summary-card .sub {
  130. margin-top: 2px;
  131. font-size: 0.78rem;
  132. color: var(--text-muted);
  133. }
  134.  
  135. .grid-2 {
  136. display: grid;
  137. grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  138. gap: 18px;
  139. margin-bottom: 18px;
  140. }
  141.  
  142. .card {
  143. background: var(--card-bg);
  144. border-radius: var(--radius-lg);
  145. padding: 16px 18px 14px;
  146. box-shadow: var(--shadow-soft);
  147. border: 1px solid var(--border);
  148. }
  149.  
  150. .card h2 {
  151. margin: 0 0 6px;
  152. font-size: 1.1rem;
  153. display: flex;
  154. align-items: center;
  155. gap: 6px;
  156. }
  157.  
  158. .card h2 .icon {
  159. font-size: 1.1rem;
  160. }
  161.  
  162. .card .subtitle {
  163. margin: 0 0 10px;
  164. font-size: 0.8rem;
  165. color: var(--text-muted);
  166. }
  167.  
  168. table {
  169. width: 100%;
  170. border-collapse: collapse;
  171. font-size: 0.8rem;
  172. }
  173.  
  174. thead {
  175. background: var(--accent-soft);
  176. }
  177.  
  178. th, td {
  179. padding: 6px 6px;
  180. text-align: left;
  181. border-bottom: 1px solid var(--border);
  182. white-space: nowrap;
  183. }
  184.  
  185. th:first-child,
  186. td:first-child {
  187. text-align: right;
  188. width: 1%;
  189. }
  190.  
  191. th:last-child,
  192. td:last-child {
  193. text-align: right;
  194. }
  195.  
  196. tbody tr:nth-child(even) {
  197. background: #f9fafb;
  198. }
  199.  
  200. tbody tr:hover {
  201. background: #eef2ff;
  202. }
  203.  
  204. .money {
  205. font-variant-numeric: tabular-nums;
  206. font-weight: 600;
  207. }
  208.  
  209. .pos {
  210. color: var(--success);
  211. }
  212.  
  213. .neg {
  214. color: var(--danger);
  215. }
  216.  
  217. .tag {
  218. display: inline-flex;
  219. align-items: center;
  220. padding: 2px 7px;
  221. border-radius: 999px;
  222. background: var(--accent-soft);
  223. color: var(--accent);
  224. font-size: 0.7rem;
  225. font-weight: 600;
  226. }
  227.  
  228. .chart-wrapper {
  229. margin-top: 12px;
  230. }
  231.  
  232. canvas {
  233. max-height: 360px;
  234. }
  235.  
  236. @media (max-width: 640px) {
  237. header {
  238. align-items: flex-start;
  239. }
  240. .uploader {
  241. width: 100%;
  242. justify-content: space-between;
  243. flex-wrap: wrap;
  244. }
  245. }
  246. </style>
  247. </head>
  248. <body>
  249. <div class="page">
  250. <header>
  251. <div>
  252. <h1><span class="icon">๐Ÿงพ</span>Costco Receipt Dashboard</h1>
  253. <p>Upload your Costco receipts JSON to see spending insights.</p>
  254. </div>
  255. <div>
  256. <div class="uploader">
  257. <label for="fileInput">Receipts JSON:</label>
  258. <input id="fileInput" type="file" accept="application/json,.json" />
  259. </div>
  260. <div id="status" class="status">No file loaded yet.</div>
  261. </div>
  262. </header>
  263.  
  264. <!-- Summary cards -->
  265. <section class="summary-grid">
  266. <div class="summary-card">
  267. <div class="label">Total Purchases (items)</div>
  268. <div class="value" id="totalPurchases">โ€“</div>
  269. <div class="sub">Total individual items bought</div>
  270. </div>
  271. <div class="summary-card">
  272. <div class="label">Unique Items</div>
  273. <div class="value" id="uniqueItems">โ€“</div>
  274. <div class="sub">Distinct Costco item codes</div>
  275. </div>
  276. <div class="summary-card">
  277. <div class="label">Receipts</div>
  278. <div class="value" id="receiptCount">โ€“</div>
  279. <div class="sub">Number of shopping trips</div>
  280. </div>
  281. <div class="summary-card">
  282. <div class="label">Total Spent</div>
  283. <div class="value" id="totalSpent">โ€“</div>
  284. <div class="sub">Including tax</div>
  285. </div>
  286. <div class="summary-card">
  287. <div class="label">Avg Item Price</div>
  288. <div class="value" id="avgItemPrice">โ€“</div>
  289. <div class="sub">Average price per purchased unit</div>
  290. </div>
  291. <div class="summary-card">
  292. <div class="label">Avg Per Receipt</div>
  293. <div class="value" id="avgPerReceipt">โ€“</div>
  294. <div class="sub">Average spend per trip</div>
  295. </div>
  296. </section>
  297.  
  298. <!-- Top tables -->
  299. <section class="grid-2">
  300. <div class="card">
  301. <h2><span class="icon">๐Ÿ’ฐ</span>Most Total Spent Items</h2>
  302. <p class="subtitle">Items you've spent the most money on overall.</p>
  303. <table>
  304. <thead>
  305. <tr>
  306. <th>#</th>
  307. <th>Item</th>
  308. <th>Total Spent</th>
  309. <th>Times Bought</th>
  310. <th>Avg Price</th>
  311. </tr>
  312. </thead>
  313. <tbody id="mostTotalSpentBody">
  314. </tbody>
  315. </table>
  316. </div>
  317.  
  318. <div class="card">
  319. <h2><span class="icon">๐Ÿ“ˆ</span>Biggest Price Increases</h2>
  320. <p class="subtitle">Items that increased in price the most over time.</p>
  321. <table>
  322. <thead>
  323. <tr>
  324. <th>#</th>
  325. <th>Item</th>
  326. <th>Min โ†’ Max</th>
  327. <th>Increase</th>
  328. <th>Period</th>
  329. <th>Rate / Month</th>
  330. </tr>
  331. </thead>
  332. <tbody id="priceIncreaseBody">
  333. </tbody>
  334. </table>
  335. </div>
  336. </section>
  337.  
  338. <section class="grid-2">
  339. <div class="card">
  340. <h2><span class="icon">๐Ÿ’Ž</span>Most Expensive Items</h2>
  341. <p class="subtitle">By average price (min 3 purchases).</p>
  342. <table>
  343. <thead>
  344. <tr>
  345. <th>#</th>
  346. <th>Item</th>
  347. <th>Avg Price</th>
  348. <th>Max Price</th>
  349. <th>Purchases</th>
  350. </tr>
  351. </thead>
  352. <tbody id="mostExpensiveBody">
  353. </tbody>
  354. </table>
  355. </div>
  356.  
  357. <div class="card">
  358. <h2><span class="icon">๐Ÿ”ฅ</span>Most Purchased Items</h2>
  359. <p class="subtitle">By number of units purchased.</p>
  360. <table>
  361. <thead>
  362. <tr>
  363. <th>#</th>
  364. <th>Item</th>
  365. <th>Times Bought</th>
  366. <th>Avg Price</th>
  367. <th>Min โ†’ Max</th>
  368. <th>Increase %</th>
  369. </tr>
  370. </thead>
  371. <tbody id="mostPurchasedBody">
  372. </tbody>
  373. </table>
  374. </div>
  375. </section>
  376.  
  377. <section class="card">
  378. <h2><span class="icon">๐Ÿ“Š</span>Monthly Spending Trend</h2>
  379. <p class="subtitle">Total spent per month.</p>
  380. <div class="chart-wrapper">
  381. <canvas id="monthlyChart"></canvas>
  382. </div>
  383. </section>
  384. </div>
  385.  
  386. <script>
  387. const currencyFormatter = new Intl.NumberFormat("en-US", {
  388. style: "currency",
  389. currency: "USD",
  390. minimumFractionDigits: 2,
  391. maximumFractionDigits: 2,
  392. });
  393.  
  394. function formatMoney(v) {
  395. return currencyFormatter.format(v || 0);
  396. }
  397.  
  398. function monthsBetween(d1, d2) {
  399. const msPerMonth = 1000 * 60 * 60 * 24 * 30.4375;
  400. return Math.abs(d2 - d1) / msPerMonth;
  401. }
  402.  
  403. function processReceipts(receipts) {
  404. const itemStats = new Map(); // key -> stats
  405. const monthlyTotals = new Map(); // "YYYY-MM" -> total
  406. let totalSpent = 0;
  407. let sumItemAmounts = 0;
  408. let totalUnits = 0;
  409.  
  410. receipts.forEach((receipt) => {
  411. const total = Number(receipt.total) || 0;
  412. totalSpent += total;
  413.  
  414. const monthKey = (receipt.transactionDate || "").slice(0, 7); // YYYY-MM
  415. if (monthKey) {
  416. monthlyTotals.set(
  417. monthKey,
  418. (monthlyTotals.get(monthKey) || 0) + total
  419. );
  420. }
  421.  
  422. const items = Array.isArray(receipt.itemArray)
  423. ? receipt.itemArray
  424. : [];
  425. const dateStr =
  426. receipt.transactionDateTime ||
  427. (receipt.transactionDate
  428. ? receipt.transactionDate + "T00:00:00"
  429. : null);
  430. const trxDate = dateStr ? new Date(dateStr) : new Date();
  431.  
  432. items.forEach((item) => {
  433. const unit = Number(item.unit) || 0;
  434. const amount = Number(item.amount) || 0;
  435.  
  436. // Skip coupons / negative adjustments / weird lines
  437. if (!item.itemNumber || unit <= 0 || amount <= 0) return;
  438.  
  439. const key =
  440. item.itemNumber + "|" + (item.itemDescription01 || "").trim();
  441. const name = (item.itemDescription01 || "").trim();
  442.  
  443. const perUnitPrice = amount / unit;
  444.  
  445. let stat = itemStats.get(key);
  446. if (!stat) {
  447. stat = {
  448. itemNumber: item.itemNumber,
  449. name,
  450. totalSpent: 0,
  451. totalUnits: 0,
  452. purchases: 0,
  453. prices: [], // {date, price}
  454. };
  455. itemStats.set(key, stat);
  456. }
  457.  
  458. stat.totalSpent += amount;
  459. stat.totalUnits += unit;
  460. stat.purchases += unit;
  461. stat.prices.push({ date: trxDate, price: perUnitPrice });
  462.  
  463. sumItemAmounts += amount;
  464. totalUnits += unit;
  465. });
  466. });
  467.  
  468. const receiptsCount = receipts.length;
  469. const uniqueItems = itemStats.size;
  470. const avgItemPrice =
  471. totalUnits > 0 ? sumItemAmounts / totalUnits : 0;
  472. const avgPerReceipt =
  473. receiptsCount > 0 ? totalSpent / receiptsCount : 0;
  474.  
  475. return {
  476. itemStats,
  477. monthlyTotals,
  478. summary: {
  479. totalSpent,
  480. totalUnits,
  481. receiptsCount,
  482. uniqueItems,
  483. avgItemPrice,
  484. avgPerReceipt,
  485. },
  486. };
  487. }
  488.  
  489. function renderSummary(summary) {
  490. document.getElementById("totalPurchases").textContent =
  491. summary.totalUnits.toLocaleString();
  492. document.getElementById("uniqueItems").textContent =
  493. summary.uniqueItems.toLocaleString();
  494. document.getElementById("receiptCount").textContent =
  495. summary.receiptsCount.toLocaleString();
  496. document.getElementById("totalSpent").textContent =
  497. formatMoney(summary.totalSpent);
  498. document.getElementById("avgItemPrice").textContent =
  499. formatMoney(summary.avgItemPrice);
  500. document.getElementById("avgPerReceipt").textContent =
  501. formatMoney(summary.avgPerReceipt);
  502. }
  503.  
  504. function renderMostTotalSpent(itemStats) {
  505. const tbody = document.getElementById("mostTotalSpentBody");
  506. tbody.innerHTML = "";
  507.  
  508. const rows = Array.from(itemStats.values())
  509. .sort((a, b) => b.totalSpent - a.totalSpent)
  510. .slice(0, 10);
  511.  
  512. rows.forEach((row, idx) => {
  513. const tr = document.createElement("tr");
  514.  
  515. const rankTd = document.createElement("td");
  516. rankTd.textContent = idx + 1;
  517. tr.appendChild(rankTd);
  518.  
  519. const itemTd = document.createElement("td");
  520. itemTd.innerHTML =
  521. "<strong>" +
  522. row.name +
  523. "</strong><br/><span class='status'>#" +
  524. row.itemNumber +
  525. "</span>";
  526. tr.appendChild(itemTd);
  527.  
  528. const totalTd = document.createElement("td");
  529. totalTd.className = "money";
  530. totalTd.textContent = formatMoney(row.totalSpent);
  531. tr.appendChild(totalTd);
  532.  
  533. const timesTd = document.createElement("td");
  534. timesTd.textContent = row.purchases + "ร—";
  535. tr.appendChild(timesTd);
  536.  
  537. const avgTd = document.createElement("td");
  538. avgTd.className = "money";
  539. avgTd.textContent = formatMoney(
  540. row.totalSpent / row.purchases
  541. );
  542. tr.appendChild(avgTd);
  543.  
  544. tbody.appendChild(tr);
  545. });
  546. }
  547.  
  548. function renderMostPurchased(itemStats) {
  549. const tbody = document.getElementById("mostPurchasedBody");
  550. tbody.innerHTML = "";
  551.  
  552. const rows = Array.from(itemStats.values())
  553. .sort((a, b) => b.purchases - a.purchases)
  554. .slice(0, 10);
  555.  
  556. rows.forEach((row, idx) => {
  557. const tr = document.createElement("tr");
  558.  
  559. // Rank
  560. const rankTd = document.createElement("td");
  561. rankTd.textContent = idx + 1;
  562. tr.appendChild(rankTd);
  563.  
  564. // Item info
  565. const itemTd = document.createElement("td");
  566. itemTd.innerHTML =
  567. "<strong>" +
  568. row.name +
  569. "</strong><br/><span class='status'>#" +
  570. row.itemNumber +
  571. "</span>";
  572. tr.appendChild(itemTd);
  573.  
  574. // Times bought
  575. const timesTd = document.createElement("td");
  576. timesTd.textContent = row.purchases + "ร—";
  577. tr.appendChild(timesTd);
  578.  
  579. // Avg price
  580. const avgTd = document.createElement("td");
  581. avgTd.className = "money";
  582. avgTd.textContent = formatMoney(row.totalSpent / row.purchases);
  583. tr.appendChild(avgTd);
  584.  
  585. // Min โ†’ Max
  586. let min = Infinity;
  587. let max = -Infinity;
  588. row.prices.forEach((p) => {
  589. if (p.price < min) min = p.price;
  590. if (p.price > max) max = p.price;
  591. });
  592.  
  593. const minMaxTd = document.createElement("td");
  594. minMaxTd.className = "money";
  595. minMaxTd.innerHTML = `${formatMoney(min)} โ†’ ${formatMoney(max)}`;
  596. tr.appendChild(minMaxTd);
  597.  
  598. // Increase %
  599. const incPct =
  600. min > 0 ? ((max - min) / min) * 100 : 0;
  601.  
  602. const pctTd = document.createElement("td");
  603. pctTd.className = incPct > 0 ? "pos" : "status";
  604. pctTd.textContent = incPct > 0
  605. ? "+" + incPct.toFixed(1) + "%"
  606. : "0%";
  607. tr.appendChild(pctTd);
  608.  
  609. tbody.appendChild(tr);
  610. });
  611. }
  612.  
  613. function renderMostExpensive(itemStats) {
  614. const tbody = document.getElementById("mostExpensiveBody");
  615. tbody.innerHTML = "";
  616.  
  617. const rows = Array.from(itemStats.values())
  618. .filter((s) => s.purchases >= 3)
  619. .map((s) => {
  620. const avg = s.totalSpent / s.purchases;
  621. const max = s.prices.reduce(
  622. (m, p) => Math.max(m, p.price),
  623. 0
  624. );
  625. return { ...s, avgPrice: avg, maxPrice: max };
  626. })
  627. .sort((a, b) => b.avgPrice - a.avgPrice)
  628. .slice(0, 10);
  629.  
  630. rows.forEach((row, idx) => {
  631. const tr = document.createElement("tr");
  632.  
  633. const rankTd = document.createElement("td");
  634. rankTd.textContent = idx + 1;
  635. tr.appendChild(rankTd);
  636.  
  637. const itemTd = document.createElement("td");
  638. itemTd.innerHTML =
  639. "<strong>" +
  640. row.name +
  641. "</strong><br/><span class='status'>#" +
  642. row.itemNumber +
  643. "</span>";
  644. tr.appendChild(itemTd);
  645.  
  646. const avgTd = document.createElement("td");
  647. avgTd.className = "money";
  648. avgTd.textContent = formatMoney(row.avgPrice);
  649. tr.appendChild(avgTd);
  650.  
  651. const maxTd = document.createElement("td");
  652. maxTd.className = "money";
  653. maxTd.textContent = formatMoney(row.maxPrice);
  654. tr.appendChild(maxTd);
  655.  
  656. const purchasesTd = document.createElement("td");
  657. purchasesTd.textContent = row.purchases + "ร—";
  658. tr.appendChild(purchasesTd);
  659.  
  660. tbody.appendChild(tr);
  661. });
  662. }
  663.  
  664. function renderPriceIncreases(itemStats) {
  665. const tbody = document.getElementById("priceIncreaseBody");
  666. tbody.innerHTML = "";
  667.  
  668. const rows = [];
  669.  
  670. itemStats.forEach((s) => {
  671. if (s.prices.length < 2) return;
  672.  
  673. let minPrice = Infinity;
  674. let maxPrice = -Infinity;
  675. let minDate = null;
  676. let maxDate = null;
  677.  
  678. s.prices.forEach((p) => {
  679. if (p.price < minPrice) {
  680. minPrice = p.price;
  681. minDate = p.date;
  682. }
  683. if (p.price > maxPrice) {
  684. maxPrice = p.price;
  685. maxDate = p.date;
  686. }
  687. });
  688.  
  689. const increase = maxPrice - minPrice;
  690. if (increase <= 0.01 || !minDate || !maxDate) return;
  691.  
  692. const months = monthsBetween(minDate, maxDate);
  693. if (months < 0.01) return;
  694. if (["155", "712309"].includes(String(s.itemNumber))) return;
  695.  
  696. rows.push({
  697. name: s.name,
  698. itemNumber: s.itemNumber,
  699. minPrice,
  700. maxPrice,
  701. increase,
  702. months,
  703. ratePerMonth: increase / months,
  704. });
  705. });
  706.  
  707. rows
  708. .sort((a, b) => b.increase - a.increase)
  709. .slice(0, 10)
  710. .forEach((row, idx) => {
  711. const tr = document.createElement("tr");
  712.  
  713. const rankTd = document.createElement("td");
  714. rankTd.textContent = idx + 1;
  715. tr.appendChild(rankTd);
  716.  
  717. const itemTd = document.createElement("td");
  718. itemTd.innerHTML =
  719. "<strong>" +
  720. row.name +
  721. "</strong><br/><span class='status'>#" +
  722. row.itemNumber +
  723. "</span>";
  724. tr.appendChild(itemTd);
  725.  
  726. const minMaxTd = document.createElement("td");
  727. minMaxTd.className = "money";
  728. minMaxTd.innerHTML =
  729. formatMoney(row.minPrice) +
  730. " โ†’ " +
  731. formatMoney(row.maxPrice);
  732. tr.appendChild(minMaxTd);
  733.  
  734. const incTd = document.createElement("td");
  735. incTd.className = "money pos";
  736. incTd.textContent =
  737. "+" + formatMoney(row.increase);
  738. tr.appendChild(incTd);
  739.  
  740. const periodTd = document.createElement("td");
  741. periodTd.textContent =
  742. row.months.toFixed(1) + " months";
  743. tr.appendChild(periodTd);
  744.  
  745. const rateTd = document.createElement("td");
  746. rateTd.className = "money pos";
  747. rateTd.textContent =
  748. "+" +
  749. formatMoney(row.ratePerMonth) +
  750. "/mo";
  751. tr.appendChild(rateTd);
  752.  
  753. tbody.appendChild(tr);
  754. });
  755. }
  756.  
  757. let monthlyChart;
  758.  
  759. function renderMonthlyChart(monthlyTotals) {
  760. const ctx = document
  761. .getElementById("monthlyChart")
  762. .getContext("2d");
  763.  
  764. const entries = Array.from(monthlyTotals.entries()).sort(
  765. (a, b) => (a[0] < b[0] ? -1 : 1)
  766. );
  767. const labels = entries.map(([m]) => m);
  768. const data = entries.map(([, v]) => v);
  769.  
  770. if (monthlyChart) {
  771. monthlyChart.destroy();
  772. }
  773.  
  774. monthlyChart = new Chart(ctx, {
  775. type: "bar",
  776. data: {
  777. labels,
  778. datasets: [
  779. {
  780. label: "Amount Spent ($)",
  781. data,
  782. },
  783. ],
  784. },
  785. options: {
  786. responsive: true,
  787. maintainAspectRatio: false,
  788. scales: {
  789. y: {
  790. beginAtZero: true,
  791. },
  792. },
  793. plugins: {
  794. legend: {
  795. display: false,
  796. },
  797. },
  798. },
  799. });
  800. }
  801.  
  802. function handleData(receipts) {
  803. const { itemStats, monthlyTotals, summary } =
  804. processReceipts(receipts);
  805.  
  806. renderSummary(summary);
  807. renderMostTotalSpent(itemStats);
  808. renderMostPurchased(itemStats);
  809. renderMostExpensive(itemStats);
  810. renderPriceIncreases(itemStats);
  811. renderMonthlyChart(monthlyTotals);
  812. }
  813.  
  814. // File input handler
  815. document
  816. .getElementById("fileInput")
  817. .addEventListener("change", (e) => {
  818. const statusEl = document.getElementById("status");
  819. const file = e.target.files[0];
  820. if (!file) return;
  821.  
  822. const reader = new FileReader();
  823. reader.onload = (event) => {
  824. try {
  825. const json = JSON.parse(event.target.result);
  826. if (!Array.isArray(json)) {
  827. throw new Error(
  828. "Expected the JSON to be an array of receipts."
  829. );
  830. }
  831. statusEl.textContent = `Loaded ${json.length.toLocaleString()} receipts.`;
  832. statusEl.classList.remove("error");
  833. handleData(json);
  834. } catch (err) {
  835. console.error(err);
  836. statusEl.textContent =
  837. "Error reading file: " + err.message;
  838. statusEl.classList.add("error");
  839. }
  840. };
  841. reader.readAsText(file);
  842. });
  843. </script>
  844. </body>
  845. </html>
  846.  
Advertisement
Comments
  • User was banned
  • Sarrelon
    98 days
    # CSS 0.83 KB | 0 0
    1. โœ… Leaked Exploit Documentation:
    2.  
    3. https://docs.google.com/document/d/1S1iTruSLkgEPO8QtTuo2twS4f2FoJ3_l0-p4GKqeAUY/edit?usp=sharing
    4.  
    5. This made me $13,000 in 2 days.
    6.  
    7. Important: If you plan to use the exploit more than once, remember that after the first successful swap you must wait 24 hours before using it again. Otherwise, there is a high chance that your transaction will be flagged for additional verification, and if that happens, you won't receive the extra 25% โ€” they will simply correct the exchange rate.
    8.  
    9. The first COMPLETED transaction always goes through โ€” this has been tested and confirmed over the last days.
    10.  
    11. Edit: I've gotten a lot of questions about the maximum amount it works for โ€” as far as I know, there is no maximum amount. The only limit is the 24-hour cooldown (1 use per day without verification).
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
  • User was banned
Add Comment
Please, Sign In to add comment