// Turns the in-memory hand history into a CSV file the dealer can download for a // paper trail — a house manager or a disputed chip count needs something that // outlives the browser tab, not just a scrollable panel in the sidebar. const HandHistoryExport = { csvField(value) { const str = String(value ?? ""); return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str; }, formatTimestamp(timestamp) { try { return new Date(timestamp).toLocaleString(); } catch { return ""; } }, // Money columns go in as bare numbers. Written as "$1,250,000.00" they import as // text, so the one thing anybody opens this file to do — total a column and see // who owes what — doesn't work without cleaning every cell first. The commas were // quoted correctly, so the file parsed; it just wouldn't add up. toCsv(handHistory, standings = [], { notes = "" } = {}) { const rows = [["Hand #", "Time", "Pot", "Payouts"]]; // handHistory is stored newest-first; the export reads better oldest-first. [...handHistory].reverse().forEach(entry => { const payouts = (entry.payouts || []) .map(p => `${p.label}: ${Money.format(p.amount)}`) .join("; "); rows.push([ entry.handNumber, this.formatTimestamp(entry.timestamp), Money.forSheet(entry.pot), payouts, ]); }); // The settle-up numbers go in the same file as the hands they came from, so a // dealer handing over one export isn't asked for a second one at cash-out. if (standings.length > 0) { rows.push([]); rows.push(["Session Standings"]); // Bought in and chips now are what a cash-out is settled from; paid in and // won describe the hands. Both belong in the file the dealer hands over. // // Notes go last so the money columns keep the shape a spreadsheet expects. // "IOU: $20 to Ann" is exactly the kind of thing that has to be in the file // somebody settles up from, and it was only ever visible inside that seat's // own editor. rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down", "Notes"]); standings.forEach(row => { rows.push([ row.name, Money.forSheet(row.in), Money.forSheet(row.won), Money.forSheet(row.net), Money.forSheet(row.boughtIn || 0), Money.forSheet(row.chipsNow || 0), Money.forSheet(row.cashPosition || 0), row.notes || "", ]); }); // And the answer the standings are read to get to. Whoever opens this file // wants the payment list, not to redo the arithmetic in a spreadsheet. const plan = SettleUp.plan(standings); if (plan.payments.length > 0) { rows.push([]); rows.push(["Settle Up"]); rows.push(["Pays", "Receives", "Amount"]); plan.payments.forEach(payment => { rows.push([payment.from, payment.to, Money.forSheet(payment.amount)]); }); if (!plan.balanced) { // The file is the paper trail, so the caveat travels with it rather // than living only in a sidebar nobody exported. rows.push([ plan.shortBy > 0 ? `Unpaid: ${Money.forSheet(plan.shortBy)} — more chips in play than cash taken` : `Unclaimed: ${Money.forSheet(plan.overBy)} — more cash taken than chips in play`, ]); } } } // The table's own notes — side bets, IOUs, the promo running until midnight. // The dealer typed them because they matter to the night, and the export is the // thing that outlives the browser tab; leaving them behind meant the one place // the context was written down was the one place it couldn't be handed over. // csvField quotes the line breaks, so a multi-line note survives as one cell. if (notes && notes.trim()) { rows.push([]); rows.push(["Table Notes"]); rows.push([notes.trim()]); } return rows.map(row => row.map(f => this.csvField(f)).join(",")).join("\r\n"); }, fileName() { const now = new Date(); const pad = n => String(n).padStart(2, "0"); const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`; return `holdem-hand-history-${stamp}.csv`; }, download(handHistory, standings = [], options = {}) { const csv = this.toCsv(handHistory, standings, options); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = this.fileName(); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }, }; if (typeof module !== "undefined" && module.exports) { module.exports = HandHistoryExport; }