| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Chip In The Pot</title> |
| <style> |
| :root { --primary: #003366; --secondary: #f39c12; --success: #27ae60; --danger: #e74c3c; --dark: #2c3e50; --light: #ecf0f1; --purple: #9b59b6; --blue: #3498db; } |
| body { |
| font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; |
| background-color: #f4f7f6; |
| margin: 0; |
| padding: 0; |
| color: var(--dark); |
| text-align: left; |
| } |
| |
| .container { max-width: 1100px; margin: 20px auto; padding: 20px; } |
| .header { background: var(--primary); color: white; padding: 30px; border-radius: 12px; text-align: center; margin-bottom: 20px; } |
| .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin-bottom: 20px; position: relative; } |
| .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } |
| |
| .win-grid { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } |
| .win-btn { text-decoration: none; padding: 8px 15px; border-radius: 6px; text-align: center; font-weight: bold; transition: 0.2s; font-size: 0.85em; color: white; flex: 1 1 auto; max-width: 200px; } |
| .win-btn:hover { opacity: 0.9; transform: translateY(-2px); } |
| .bg-olg { background: #004d40; } |
| .bg-5050 { background: #c0392b; } |
| |
| .measuring-cup-wrapper { display: flex; align-items: flex-end; margin-bottom: 35px; padding-right: 140px; margin-top: 25px; } |
| .cup-container { width: 100%; height: 220px; border: 3px solid var(--dark); border-top: none; border-radius: 0 0 15px 15px; position: relative; overflow: visible; background: #fff; flex-grow: 1; } |
| .cup-fill { position: absolute; bottom: 0; left: 0; width: 100%; background: linear-gradient(to top, #27ae60, #2ecc71); transition: height 0.8s ease-out; z-index: 1; } |
| .tier-marker { position: absolute; right: -140px; width: 135px; border-top: 1px dashed #666; font-size: 10px; padding-top: 2px; color: #444; pointer-events: none; text-align: left; } |
| .cup-label { position: absolute; top: 10px; left: 10px; z-index: 3; font-weight: bold; background: rgba(255,255,255,0.85); padding: 2px 6px; border-radius: 4px; border: 1px solid #ddd; } |
| |
| .encouragement { font-size: 0.85em; margin-top: 10px; font-weight: bold; color: var(--primary); min-height: 1.2em; } |
| .shortfall { color: var(--danger); animation: pulse 2s infinite; } |
| @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } |
| |
| .progress-bar { height: 24px; background: #eee; border-radius: 12px; margin: 10px 0; overflow: hidden; border: 1px solid #ddd; position: relative; } |
| .progress-inner { height: 100%; background: var(--success); width: 0%; transition: width 0.5s; } |
| .progress-text { position: absolute; width: 100%; text-align: center; font-size: 0.8em; font-weight: bold; line-height: 24px; color: var(--dark); z-index: 2; } |
| |
| .eb-list { font-size: 0.75em; margin-top: 10px; border-top: 1px solid #eee; padding-top: 8px; } |
| .eb-item { display: flex; justify-content: space-between; margin-bottom: 3px; } |
| .eb-ended { color: #aaa; text-decoration: line-through; } |
| .eb-active { color: var(--success); font-weight: bold; } |
| |
| .cycle-info { font-size: 0.8em; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 8px; margin-top: 10px; display: flex; justify-content: space-between; color: #555; } |
| .cycle-date { font-weight: bold; color: var(--primary); } |
| |
| |
| .wheel-container { width: 280px; height: 280px; border-radius: 50%; border: 6px solid #fff; box-shadow: 0 0 20px rgba(0,0,0,0.3); margin: 20px auto; position: relative; background: #eee; transition: transform 10s cubic-bezier(0.1, 0.9, 0.1, 1.0); } |
| .wheel-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60px; height: 60px; background: white; border-radius: 50%; z-index: 2; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; box-shadow: inset 0 0 5px rgba(0,0,0,0.1); border: 2px solid #ddd; } |
| .wheel-pointer { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 20px solid transparent; border-right: 20px solid transparent; border-top: 30px solid var(--danger); z-index: 3; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.3)); } |
| |
| .wheel-legend { font-size: 0.8em; margin-top: 15px; text-align: left; border-top: 1px solid #eee; padding-top: 10px; } |
| .legend-item { display: flex; justify-content: space-between; margin-bottom: 3px; } |
| .legend-color { width: 10px; height: 10px; display: inline-block; margin-right: 5px; border-radius: 2px; } |
| |
| |
| #winnerOverlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 99999; text-align: center; color: white; flex-direction: column; justify-content: center; align-items: center; } |
| #winnerName { font-size: 4em; font-weight: bold; text-shadow: 0 0 20px #FFD700; color: #FFD700; animation: popIn 0.5s ease-out; } |
| #winnerAmount { font-size: 2em; margin-top: 10px; color: #fff; } |
| @keyframes popIn { 0% { transform: scale(0); } 80% { transform: scale(1.1); } 100% { transform: scale(1); } } |
| |
| |
| .confetti { position: fixed; width: 10px; height: 10px; background-color: #f00; animation: confetti-fall 3s linear infinite; } |
| @keyframes confetti-fall { 0% { transform: translateY(-100vh) rotate(0deg); } 100% { transform: translateY(100vh) rotate(360deg); } } |
| |
| |
| .winner-row { background-color: #fff9c4 !important; border: 2px solid #FFD700; animation: flashRow 2s infinite; } |
| @keyframes flashRow { 0% { background-color: #fff9c4; } 50% { background-color: #fff; } 100% { background-color: #fff9c4; } } |
| |
| table { width: 100%; border-collapse: collapse; margin-top: 15px; font-size: 0.85em; } |
| th, td { padding: 10px; text-align: left; border-bottom: 1px solid #eee; } |
| .btn { padding: 8px 15px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: 0.3s; margin: 4px; font-size: 0.9em; } |
| .btn-primary { background: var(--primary); color: white; } |
| .btn-success { background: var(--success); color: white; } |
| .btn-danger { background: var(--danger); color: white; } |
| .btn-asset { background: var(--purple); color: white; } |
| .casino-card { background: #1a1a1a; color: #ffd700; border: 2px solid #ffd700; text-align: center; } |
| .leader-section { border: 2px dashed var(--primary); background: #fdfdfd; padding: 25px; margin-top: 40px; display: none; border-radius: 12px; } |
| input, select, textarea { width: 100%; padding: 10px; margin-top: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-family: inherit; } |
| |
| .badge { padding: 2px 5px; border-radius: 4px; font-size: 9px; font-weight: bold; color: white; margin-right: 3px; display: inline-block; margin-bottom: 2px; } |
| .bg-eb { background: #e67e22; } |
| .bg-ft { background: var(--purple); border: 1px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } |
| .bg-verified { background: var(--success); } |
| .bg-rollover { background: var(--blue); } |
| |
| .sortable { cursor: pointer; user-select: none; color: var(--primary); } |
| .sortable:hover { text-decoration: underline; background: #eee; } |
| |
| #toast { visibility: hidden; min-width: 250px; background-color: #333; color: #fff; text-align: center; border-radius: 8px; padding: 16px; position: fixed; z-index: 9999; left: 50%; bottom: 30px; transform: translateX(-50%); font-weight: bold; } |
| #toast.show { visibility: visible; animation: fadein 0.5s, fadeout 0.5s 2.5s; } |
| |
| .payout-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; } |
| details summary::-webkit-details-marker { display:none; } |
| |
| #reportModal { display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); } |
| .modal-content { background: white; margin: 5% auto; padding: 30px; width: 90%; max-width: 1000px; border-radius: 12px; max-height: 90vh; overflow-y: auto; position: relative; } |
| .close-modal { position: absolute; right: 20px; top: 15px; font-size: 24px; cursor: pointer; color: var(--danger); font-weight: bold; } |
| </style> |
| </head> |
| <body> |
| <div id="toast">Action Confirmed</div> |
|
|
| <div id="winnerOverlay" onclick="closeWinnerOverlay()"> |
| <div style="font-size: 5em;">🎉</div> |
| <h2>CASINO POT WINNER</h2> |
| <div id="winnerName">PLAYER NAME</div> |
| <div id="winnerAmount">$0.00</div> |
| <p style="margin-top:20px; font-size:0.8em; color:#ccc;">(Click to Close & Secure Funds)</p> |
| </div> |
|
|
| <div id="reportModal"> |
| <div class="modal-content"> |
| <span class="close-modal" onclick="document.getElementById('reportModal').style.display='none'">× Close</span> |
| <h2 id="modalTitle" style="color:var(--primary); margin-top:0;">Portfolio Report</h2> |
| <pre id="modalBody" style="font-family:monospace; background:#f4f7f6; padding:20px; white-space:pre-wrap; border-radius:8px; border:1px solid #ddd;"></pre> |
| <button class="btn btn-primary" onclick="window.print()">Print This Screen</button> |
| </div> |
| </div> |
|
|
| <div class="container"> |
| <div class="header"> |
| <h1>Chip In The Pot</h1> |
| <p>Managed Group Pool | Professional Multi-Asset Accountability Ledger</p> |
| |
| <div class="card" style="background: rgba(255,255,255,0.9); color: var(--dark); text-align: left; margin-top: 20px;"> |
| <details> |
| <summary style="cursor:pointer; font-weight:bold; font-size: 1.1em; color: var(--primary); padding: 5px;">🚀 User Guide: Quick Start</summary> |
| <div style="padding: 15px; border-top: 1px solid #ddd; margin-top: 10px;"> |
| |
| <div style="background: #eafaf1; border-left: 4px solid var(--success); padding: 15px; margin-bottom: 15px;"> |
| <h4 style="margin-top:0; color:var(--success);">💳 Payment Instructions & Fees</h4> |
| <ul style="margin:0; padding-left:20px; font-size:0.9em; line-height:1.6;"> |
| <li><strong>Minimum Entry:</strong> $11.00 (CAD)</li> |
| <li><strong>How to Pay:</strong> Send Interac E-Transfer to <strong style="color:var(--primary);">chipinthepot@gmail.com</strong></li> |
| <li><strong>Important:</strong> Please include your <strong>Full Name</strong> in the e-transfer message notes for faster verification.</li> |
| <li><strong>The Breakdown:</strong> |
| <ul style="margin-top:5px; margin-bottom:5px;"> |
| <li><strong>$10.00:</strong> Direct Equity Shares (Tickets)</li> |
| <li><strong>$0.50:</strong> Admin Fee (Site Maintenance)</li> |
| <li><strong>$0.50:</strong> Strategic Casino Pot (Bonus Games)</li> |
| </ul> |
| </li> |
| <li><strong>Fees apply to EVERY transfer:</strong> Multiple small payments will incur multiple fees. Sending one larger amount saves you money.</li> |
| <li><strong>No Advance Payments:</strong> Payments are for the CURRENT cycle only. Future months will be returned.</li> |
| <li><strong>Collection Cycle:</strong> Opens on the <strong>15th</strong> of each month.</li> |
| </ul> |
| </div> |
|
|
| <details style="margin-bottom: 10px; border: 1px solid #eee; border-radius: 6px; padding: 10px;"> |
| <summary style="cursor:pointer; font-weight:bold; color: var(--secondary);">💰 Shares & Casino Logic</summary> |
| <div style="font-size: 0.85em; padding-top: 10px; line-height: 1.5;"> |
| • Every $11.00 entry buys 10.0 Shares.<br> |
| • Winnings are split exactly by your percentage ownership of the pool.<br> |
| • The $0.50 Casino Pot builds up for special high-risk/high-reward side bets. |
| </div> |
| </details> |
|
|
| <details style="margin-bottom: 10px; border: 1px solid #eee; border-radius: 6px; padding: 10px;"> |
| <summary style="cursor:pointer; font-weight:bold; color: var(--secondary);">🌊 Water Waterfall (Rounds)</summary> |
| <div style="font-size: 0.85em; padding-top: 10px; line-height: 1.5;"> |
| • Funds fill city cups in order: Timmins → Sudbury → T-Bay → OLG.<br> |
| • One full sweep costs <strong>$315.00</strong>. Beyond that, Round 2 begins automatically. |
| </div> |
| </details> |
|
|
| <details style="border: 1px solid #eee; border-radius: 6px; padding: 10px;"> |
| <summary style="cursor:pointer; font-weight:bold; color: var(--danger);">⚖️ Legal & Verification Risk</summary> |
| <div style="font-size: 0.85em; padding-top: 10px; line-height: 1.5;"> |
| • <strong>Payment Verification:</strong> Eligibility is only "Verified" once the Leader confirms the e-transfer.<br> |
| • <strong>Deadline Rules:</strong> You must be verified before a city's Early Bird deadline to be eligible for that prize.<br> |
| • <strong>Data Integrity:</strong> All data is permanently logged in the "Winnings History" ledger for audit. |
| </div> |
| </details> |
| </div> |
| </details> |
|
|
| <details style="margin-top:10px;"> |
| <summary style="cursor:pointer; font-weight:bold; font-size: 1.1em; color: var(--success); padding: 5px;">🎱 Check Winning Numbers</summary> |
| <div style="padding: 15px; border-top: 1px solid #ddd; margin-top: 10px;"> |
| <p style="font-size:0.85em; color:#666; margin-bottom:10px;">Click below to verify official results in a new tab:</p> |
| |
| <strong style="color:var(--danger); display:block; margin-bottom:5px;">Regional 50/50s</strong> |
| <div class="win-grid"> |
| <a href="https://timminsdistrict5050.ca/" target="_blank" class="win-btn bg-5050">Timmins Hospital</a> |
| <a href="https://hsn5050.ca/" target="_blank" class="win-btn bg-5050">Sudbury (HSN)</a> |
| <a href="https://www.thunderbay5050.ca/winners" target="_blank" class="win-btn bg-5050">Thunder Bay</a> |
| </div> |
|
|
| <strong style="color:var(--success); display:block; margin-top:15px; margin-bottom:5px;">OLG Classics</strong> |
| <div class="win-grid"> |
| <a href="https://www.olg.ca/en/lottery/play-lotto-max-encore/past-results.html" target="_blank" class="win-btn bg-olg">Lotto Max</a> |
| <a href="https://www.olg.ca/en/lottery/play-lotto-649-encore/past-results.html" target="_blank" class="win-btn bg-olg">Lotto 6/49</a> |
| <a href="https://www.olg.ca/en/lottery/play-daily-grand-encore/past-results.html" target="_blank" class="win-btn bg-olg">Daily Grand</a> |
| <a href="https://www.olg.ca/en/lottery/play-ontario-49-encore/past-results.html" target="_blank" class="win-btn bg-olg">Ontario 49</a> |
| <a href="https://www.olg.ca/en/lottery/play-lottario-encore/past-results.html" target="_blank" class="win-btn bg-olg">Lottario</a> |
| </div> |
| </div> |
| </details> |
| </div> |
| <h2 id="roundStatus" style="color:var(--secondary); margin-top: 20px;">ROUND 1 ACTIVE</h2> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <h3>Portfolio Treasury</h3> |
| <p>Gross Funding (Active): <strong id="grossTotal">$0.00</strong></p> |
| <p>Net Ticket Liquidity: <strong id="totalPool">$0.00</strong></p> |
| <p>Active Shares: <strong id="totalShares">0.0</strong></p> |
| |
| <div class="progress-bar"> |
| <div class="progress-text" id="roundProgressLabel">Round 1: $0 / $315</div> |
| <div class="progress-inner" id="globalProgress"></div> |
| </div> |
| </div> |
| <div class="card casino-card"> |
| <h3>🎰 Casino Pot 🎰</h3> |
| <h2 id="casinoBalance" style="margin:10px 0;">$0.00</h2> |
| |
| <div id="lastWinnerDisplay" style="display:none; border:1px solid #ffd700; padding:10px; border-radius:8px; margin-bottom:10px; background:#333;"> |
| 👑 <strong>Last Winner:</strong> <span id="lastWinnerName" style="color:#ffd700;">-</span> |
| </div> |
|
|
| <div id="nextSpinDisplay" style="color:#FFD700; font-weight:bold; margin-bottom:10px; font-size:0.9em;"></div> |
|
|
| <div id="casinoWheelContainer" style="display:none; position:relative;"> |
| <div class="wheel-pointer"></div> |
| <div class="wheel-container" id="casinoWheel"> |
| <div class="wheel-center">🎲</div> |
| </div> |
| <button class="btn btn-success" onclick="spinWheel()" style="display:block; margin:10px auto; font-weight:bold; background:#e74c3c; border:2px solid #fff; box-shadow:0 2px 5px rgba(0,0,0,0.5);">🎲 SPIN THE WHEEL</button> |
| <div class="wheel-legend" id="wheelLegend"></div> |
| </div> |
| |
| <div id="casinoEmptyState" style="font-size:0.9em; padding:20px; display:none;"> |
| <span style="color:#888;">Pot is empty. Add funds to spin!</span> |
| </div> |
|
|
| <p id="casinoStatus" style="font-size:0.9em; font-style:italic; margin-top:15px;">💥 One lucky winner takes the whole pot! Is it you? 💥</p> |
| </div> |
| </div> |
|
|
| <h3>Jackpot Targets & Deadlines</h3> |
| <div class="grid" id="cupDisplay"></div> |
|
|
| <div class="card"> |
| <h3>Group Asset Vault (Read-Only)</h3> |
| <div id="assetVaultTable"><p><small>No free ticket assets recorded.</small></p></div> |
| </div> |
|
|
| <div class="card"> |
| <h3>Group Winnings History (Detailed Ledger)</h3> |
| <div id="winHistory"><p><small>No results recorded for this month yet.</small></p></div> |
| </div> |
|
|
| <div class="card"> |
| <h3>Participant Equity & Total Winnings Tracker</h3> |
| <table> |
| <thead><tr><th>Rank</th><th>Participant</th><th>Shares</th><th>Total Won</th><th>Equity %</th><th>Status & Eligibility</th></tr></thead> |
| <tbody id="equityBody"></tbody> |
| </table> |
| </div> |
|
|
| <button class="btn btn-primary" onclick="accessLeader()">Leader Control Panel</button> |
|
|
| <div id="leaderPanel" class="leader-section"> |
| <h2 style="color:var(--primary);">Leader Control Center</h2> |
| |
| <div class="card" style="margin-bottom: 20px; border: 2px solid #666;"> |
| <h4>Data System Management (Secure)</h4> |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;"> |
| <span style="font-weight:bold; color:var(--dark);">Admin Fees Wallet: <span id="adminWallet" style="color:var(--success);">$0.00</span></span> |
| <button class="btn" onclick="withdrawAdmin()" style="background:#d35400; color:white; font-size:0.8em;">Withdraw Fees</button> |
| </div> |
| <div style="display:flex; gap:5px; flex-wrap:wrap;"> |
| <button class="btn" onclick="exportDB()" style="background:#666; color:white; flex-grow:1;">Backup Data</button> |
| <button class="btn" onclick="document.getElementById('importFile').click()" style="background:#2980b9; color:white; flex-grow:1;">Load Data</button> |
| <input type="file" id="importFile" style="display:none" onchange="importDB(event)"> |
| <button class="btn" onclick="withdrawCasino()" style="background:#f39c12; color:white; flex-grow:1;">Withdraw Casino</button> |
| <button class="btn" onclick="generateAuditReport()" style="background:#2ecc71; color:white; flex-grow:1;">Audit Report</button> |
| <button class="btn" onclick="generateTestLog()" style="background:#9b59b6; color:white; flex-grow:1;">Print Test Log</button> |
| <button class="btn" onclick="resetPool()" style="background:#c0392b; color:white; flex-grow:1;">Start New Month / Rollover</button> |
| <button class="btn" onclick="factoryReset()" style="background:#000; color:red; flex-grow:1; border:1px solid red;">⚠️ FACTORY RESET</button> |
| </div> |
| </div> |
|
|
| <div class="card" style="border: 2px dashed var(--blue); background:#eef2f7;"> |
| <h4 style="color:var(--blue);">⏳ Upcoming Cycle Queue (Hidden from Public)</h4> |
| <div id="holdingTankTable"><small>No future members pending.</small></div> |
| </div> |
|
|
| <div class="card" style="border: 2px solid var(--secondary);"> |
| <h4 style="color:var(--secondary);">🎰 Casino Configuration</h4> |
| |
| <div style="background:#fef5e7; padding:10px; border-radius:6px; margin-bottom:15px; border:1px solid var(--secondary);"> |
| <label style="font-weight:bold; color:var(--secondary);">📅 MASTER DEADLINE (Updates All Buckets)</label> |
| <div style="display:flex; gap:10px; margin-top:5px;"> |
| <input type="date" id="masterDeadlineInput"> |
| <button class="btn btn-primary" onclick="setMasterDeadline()">Set All</button> |
| </div> |
| <small style="color:#666;">Use this to quickly rollover dates for the new month.</small> |
| </div> |
|
|
| <div class="grid" style="grid-template-columns: 1fr 1fr; gap:10px;"> |
| <div> |
| <label style="font-size:0.7em;">Next Spin Date/Time:</label> |
| <input type="datetime-local" id="confNextSpin" oninput="updateCasinoConfig()"> |
| </div> |
| <div> |
| <label style="font-size:0.7em;">Custom Status Message:</label> |
| <input type="text" id="confStatusMsg" placeholder="e.g. Christmas Bonus Spin!" oninput="updateCasinoConfig()"> |
| </div> |
| </div> |
| <div class="grid" style="grid-template-columns: 1fr 1fr; gap:10px; margin-top:10px;"> |
| <div> |
| <label style="font-size:0.7em;">Manual Last Winner Name:</label> |
| <input type="text" id="confWinnerName" placeholder="Name" oninput="updateCasinoConfig()"> |
| </div> |
| <div> |
| <label style="font-size:0.7em;">Manual Last Winner Amount:</label> |
| <input type="number" id="confWinnerAmount" placeholder="Amount" oninput="updateCasinoConfig()"> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card" id="assetManager" style="border: 2px solid var(--purple);"> |
| <h4>(Step 1) 🎟️ Register Free Ticket</h4> |
| <label>Game Won From:</label> |
| <select id="assetGame"> |
| <option value="Lotto Max">Lotto Max</option> |
| <option value="Lotto 649">Lotto 649</option> |
| <option value="Daily Grand">Daily Grand</option> |
| <option value="Ontario 49">Ontario 49</option> |
| </select> |
| <label>Date Won:</label> |
| <input type="date" id="assetDate"> |
| <button class="btn btn-asset" onclick="addAsset()" style="width:100%; margin-top:10px;">Deposit into Vault</button> |
| </div> |
|
|
| <div class="card" style="border: 2px solid var(--purple);"> |
| <h4>(Step 2) 🔒 Lock & Play Vault</h4> |
| <div id="leaderAssetVaultTable"><p><small>No assets to lock.</small></p></div> |
| </div> |
|
|
| <div class="card" id="payoutCalc" style="border: 2px solid var(--success);"> |
| <h4>(Step 3) 🏆 Payout Calculator</h4> |
| <label>Win Type:</label> |
| <select id="winType" onchange="toggleWinSource()"> |
| <option value="standard">Standard Draw Win</option> |
| <option value="eb">Early Bird Win</option> |
| <option value="asset">Used Free Ticket Win</option> |
| </select> |
| <div id="sourceSelector"> |
| <label>Identify Source:</label> |
| <select id="winSource"></select> |
| </div> |
| <input type="number" id="lPrizeAmount" placeholder="Prize Amount ($) - 0 is allowed"> |
| <button class="btn btn-success" onclick="calculatePayouts()" style="width:100%; margin-top:10px;">Divide Winnings</button> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h4>Add & Verify Member</h4> |
| <input type="text" id="lName" placeholder="Full Name"> |
| <input type="email" id="lEmail" placeholder="Email Address"> |
| <input type="number" id="lAmount" placeholder="Total Amount ($)"> |
| <input type="text" id="lRef" placeholder="Interac Ref # (e.g. C12345678)" style="border:1px solid var(--secondary);"> |
| <small style="display:block; margin-top:10px; color:#666;">Manual Bank Time:</small> |
| <input type="datetime-local" id="lManualTime"> |
| <button class="btn btn-success" onclick="verifyParticipant()" style="width:100%; margin-top:10px;">Verify & Clear Forms</button> |
| </div> |
|
|
| <div class="card" id="payoutHistoryCard" style="border: 1px solid var(--primary);"> |
| <div class="payout-nav"> |
| <button class="btn btn-primary" onclick="changePayoutView(-1)">← Previous</button> |
| <strong id="payoutTitle">Payout History Navigator</strong> |
| <button class="btn btn-primary" onclick="changePayoutView(1)">Next →</button> |
| </div> |
| <div id="payoutResults"></div> |
| </div> |
|
|
| <div class="card" style="border: 2px solid var(--danger);"> |
| <h4>🗑️ Manage Winnings (Leader Only)</h4> |
| <p style="font-size:0.8em; color:#666;">Deleting an entry here will reverse the money from everyone's account.</p> |
| <div id="adminWinHistory"></div> |
| </div> |
|
|
| <div class="card" id="adminMemberCard" style="border: 2px solid var(--primary); overflow-x: auto;"> |
| <h4>👥 Member Management (Sortable)</h4> |
| <p style="font-size:0.8em; color:#666;">Click column headers (Name, Date, etc.) to sort the list.</p> |
| <table id="adminMemberTable"> |
| <thead> |
| <tr> |
| <th class="sortable" onclick="sortMembers('display')">Name ↕</th> |
| <th class="sortable" onclick="sortMembers('email')">Email ↕</th> |
| <th class="sortable" onclick="sortMembers('refId')">Ref # ↕</th> |
| <th class="sortable" onclick="sortMembers('rawTime')">Date ↕</th> |
| <th class="sortable" onclick="sortMembers('shares')">Paid ($) ↕</th> |
| <th>Entry Type</th> |
| <th>Assign FT Game</th> |
| <th>Actions</th> |
| </tr> |
| </thead> |
| <tbody id="adminMemberBody"></tbody> |
| </table> |
| </div> |
|
|
| <div class="card" id="configCard" style="border: 2px solid var(--secondary);"> |
| <h4>⚙️ Waterfall & Draw Configuration</h4> |
| <p style="font-size:0.8em; color:#666;">Adjust cup goals and deadlines. Manage Early Birds with dedicated labels and calendar selectors.</p> |
| <div id="cupConfigEditor"></div> |
| <button class="btn btn-primary" onclick="saveData(); updateUI(); showToast('Configuration Applied!')" style="width:100%; margin-top:10px;">Apply & Save Configuration</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const BIN_ID = "6966ff7a43b1c97be92f0e1e"; |
| const API_KEY = "$2a$10$vt1C.GjcH..nUfdBlUog.e9iXve.lHmXapRtVBe2Kvc/q.IRgCkHu"; |
| |
| let audioCtx; |
| |
| let db = { |
| verifiedPool: 0, casinoPot: 0, adminFee: 0, totalShares: 0, members: [], winnings: [], assets: [], lastCasinoWinner: null, |
| config: { |
| casino: { nextSpin: "2026-02-28T23:59", customStatus: "", lastWinner: { name: "", amount: 0 } }, |
| cups: [ |
| { id: 'timmins', name: '1. Timmins 50/50', max: 75, startDate: '2026-01-15', deadline: '2026-02-28', tiers: "5,10,20,40,75", tickets: "3,10,60,250,750", ebList: [ |
| {label: '1K-A-Day', time: '2026-02-14T23:59'}, {label: '1K-A-Day', time: '2026-02-14T23:59'}, {label: '$1000', time: '2026-02-21T23:59'}, {label: '$1000', time: '2026-02-21T23:59'}, {label: '$1000', time: '2026-02-28T23:59'} |
| ] }, |
| { id: 'sudbury', name: '2. Sudbury (HSN)', max: 100, startDate: '2026-01-15', deadline: '2026-02-28', tiers: "15,25,50,75,100", tickets: "30,75,200,425,700", ebList: [ |
| {label: '$15,000 Cash', time: '2026-02-14T23:59'}, {label: '$15,000 Cash', time: '2026-02-21T23:59'}, {label: '$15,000 Cash', time: '2026-02-28T23:59'}, {label: '$15,000 Cash', time: '2026-02-28T23:59'} |
| ] }, |
| { id: 'tbay', name: '3. Thunder Bay', max: 100, startDate: '2026-01-15', deadline: '2026-02-28', tiers: "10,20,50,75,100", tickets: "5,30,150,300,500", ebList: [ |
| {label: '$10,000', time: '2026-02-14T23:59'}, {label: '$5,000', time: '2026-02-21T23:59'} |
| ] }, |
| { id: 'olg', name: '4. OLG Max Pack 40', max: 40, startDate: '2026-01-15', deadline: '2026-02-28', tiers: "10,12,15,20,40", tickets: "Jackpot Pack,Big Ones Pack,Sampler Pack,Mega Pack,Max Pack", ebList: [] } |
| ] |
| } |
| }; |
| |
| let currentPayoutIdx = -1; |
| let sortConfig = { key: 'rawTime', asc: false }; |
| const casinoColors = ['#e74c3c', '#3498db', '#f1c40f', '#9b59b6', '#2ecc71', '#34495e', '#e67e22', '#1abc9c']; |
| let latestWinner = null; |
| |
| |
| const LEADER_KEY_ENCODED = "MTE3MTQyMDMxRHRuQA=="; |
| |
| function accessLeader() { |
| if (btoa(prompt("Key:")) === LEADER_KEY_ENCODED) { |
| document.getElementById('leaderPanel').style.display = "block"; |
| renderConfigEditor(); |
| } else { |
| alert("Invalid Key."); |
| } |
| } |
| |
| |
| function playClickSound() { |
| if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); |
| const osc = audioCtx.createOscillator(); |
| const gain = audioCtx.createGain(); |
| osc.type = 'triangle'; |
| osc.frequency.setValueAtTime(800, audioCtx.currentTime); |
| osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.1); |
| gain.gain.setValueAtTime(0.1, audioCtx.currentTime); |
| gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); |
| osc.connect(gain); |
| gain.connect(audioCtx.destination); |
| osc.start(); |
| osc.stop(audioCtx.currentTime + 0.1); |
| } |
| |
| function playWinSound() { |
| if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); |
| const now = audioCtx.currentTime; |
| [523.25, 659.25, 783.99, 1046.50].forEach((freq, i) => { |
| const osc = audioCtx.createOscillator(); |
| const gain = audioCtx.createGain(); |
| osc.type = 'sine'; |
| osc.frequency.value = freq; |
| gain.gain.setValueAtTime(0.2, now + i*0.1); |
| gain.gain.exponentialRampToValueAtTime(0.01, now + i*0.1 + 1); |
| osc.connect(gain); |
| gain.connect(audioCtx.destination); |
| osc.start(now + i*0.1); |
| osc.stop(now + i*0.1 + 1); |
| }); |
| } |
| |
| async function saveData() { |
| showToast("⏳ Syncing to Cloud..."); |
| try { |
| await fetch(`https://api.jsonbin.io/v3/b/${BIN_ID}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json', 'X-Master-Key': API_KEY }, |
| body: JSON.stringify(db) |
| }); |
| showToast("✅ Cloud Saved!"); |
| } catch (err) { alert("Error saving to cloud: " + err); } |
| } |
| |
| async function loadData() { |
| showToast("⏳ Loading from Cloud..."); |
| try { |
| let response = await fetch(`https://api.jsonbin.io/v3/b/${BIN_ID}/latest`, { headers: { 'X-Master-Key': API_KEY } }); |
| if (!response.ok) throw new Error("Cloud fetch failed"); |
| let result = await response.json(); |
| if (result.record) { |
| db = result.record; |
| if (db.adminFee === undefined) db.adminFee = 0; |
| if (!db.config.casino) db.config.casino = { nextSpin: "2026-02-28T23:59", customStatus: "", lastWinner: { name: "", amount: 0 } }; |
| updateUI(); |
| showToast("✅ Data Loaded"); |
| } |
| } catch (err) { showToast("⚠️ Using Default Data"); } |
| } |
| |
| function showToast(msg) { |
| const t = document.getElementById("toast"); t.innerText = msg; t.className = "show"; |
| setTimeout(() => { t.className = t.className.replace("show", ""); }, 3000); |
| } |
| |
| function getDaysLeft(dateStr) { |
| if(!dateStr) return ""; |
| const d = new Date(dateStr).getTime(); |
| const diff = d - new Date().getTime(); |
| const days = Math.ceil(diff / (1000 * 60 * 60 * 24)); |
| if (diff < 0) return "ENDED"; |
| return days === 0 ? "⚠️ ENDS TODAY!!" : `${days} Days Left!!`; |
| } |
| |
| function sortMembers(key) { |
| if (sortConfig.key === key) sortConfig.asc = !sortConfig.asc; |
| else { sortConfig.key = key; sortConfig.asc = true; } |
| updateUI(); |
| } |
| |
| function generateTestLog() { |
| const report = `SYSTEM VALIDATION PROTOCOL: CHIP IN THE POT\nCertification Date: ${new Date().toLocaleDateString()}\n\nAdministrator Signature: ___________________________`; |
| document.getElementById('modalTitle').innerText = "System Validation Log"; |
| document.getElementById('modalBody').innerText = report; |
| document.getElementById('reportModal').style.display = 'block'; |
| } |
| |
| function renderConfigEditor() { |
| const container = document.getElementById('cupConfigEditor'); |
| if (!container) return; |
| container.innerHTML = db.config.cups.map((cup, idx) => ` |
| <div style="border-bottom: 2px solid #eee; padding: 15px 0; margin-bottom:15px; background: #fafafa; border-radius: 8px; padding-left: 10px;"> |
| <strong style="color:var(--primary); font-size: 1.1em;">Bucket ${idx+1}: ${cup.name}</strong> |
| <div class="grid" style="grid-template-columns: 1fr 1fr; gap: 10px; margin-top:10px;"> |
| <div><label style="font-size:0.7em; font-weight:bold;">Display Name:</label><input type="text" value="${cup.name}" oninput="db.config.cups[${idx}].name = this.value"></div> |
| <div><label style="font-size:0.7em; font-weight:bold;">Max Goal ($):</label><input type="number" value="${cup.max}" oninput="db.config.cups[${idx}].max = parseFloat(this.value)"></div> |
| </div> |
| <div class="grid" style="grid-template-columns: 1fr 1fr; gap: 10px; margin-top:10px;"> |
| <div><label style="font-size:0.7em; font-weight:bold;">Collection Start:</label><input type="date" value="${cup.startDate || ''}" oninput="db.config.cups[${idx}].startDate = this.value"></div> |
| <div><label style="font-size:0.7em; font-weight:bold;">Draw Deadline:</label><input type="date" value="${cup.deadline}" oninput="db.config.cups[${idx}].deadline = this.value"></div> |
| </div> |
| <div class="grid" style="grid-template-columns: 1fr 1fr; gap: 10px; margin-top:10px;"> |
| <div><label style="font-size:0.7em; font-weight:bold;">Value Tiers (e.g. 5,10,75):</label><input type="text" value="${cup.tiers}" oninput="db.config.cups[${idx}].tiers = this.value"></div> |
| <div><label style="font-size:0.7em; font-weight:bold;">Ticket Count (e.g. 10,30,750):</label><input type="text" value="${cup.tickets}" oninput="db.config.cups[${idx}].tickets = this.value"></div> |
| </div> |
| <div style="margin-top:15px; border-top: 1px solid #ddd; padding-top: 10px;"> |
| <label style="font-size:0.8em; font-weight:bold; color:var(--secondary);">🎯 Early Bird Deadlines</label> |
| ${cup.ebList.length > 0 ? ` |
| <div style="background:#eef2f7; padding:5px; border-radius:4px; margin-bottom:5px; display:flex; gap:5px; align-items:center;"> |
| <small style="white-space:nowrap; font-weight:bold;">⚡ Quick Set All EBs:</small> |
| <input type="datetime-local" id="quickEB_${idx}" style="padding:2px; font-size:0.8em;"> |
| <button class="btn btn-primary" style="padding:2px 5px; font-size:0.7em;" onclick="quickSetEB(${idx})">Set</button> |
| </div>` : ''} |
| <div id="eb-list-${idx}" style="margin-top:5px;"> |
| ${cup.ebList.map((eb, ebIdx) => ` |
| <div class="grid" style="grid-template-columns: 1fr 1.5fr auto; gap: 5px; margin-bottom:5px; align-items: center;"> |
| <input type="text" placeholder="Prize Name" value="${eb.label}" oninput="db.config.cups[${idx}].ebList[${ebIdx}].label = this.value"> |
| <input type="datetime-local" value="${eb.time}" oninput="db.config.cups[${idx}].ebList[${ebIdx}].time = this.value"> |
| <button class="btn btn-danger" style="padding: 2px 10px;" onclick="removeEB(${idx}, ${ebIdx})">×</button> |
| </div> |
| `).join('')} |
| </div> |
| <button class="btn btn-success" style="padding: 4px 10px; font-size: 0.75em; margin-top: 5px;" onclick="addEB(${idx})">+ Add Early Bird</button> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| function quickSetEB(cupIdx) { |
| const val = document.getElementById(`quickEB_${cupIdx}`).value; |
| if(!val) return; |
| if(confirm(`Update ALL Early Birds in this bucket to ${val}?`)) { |
| db.config.cups[cupIdx].ebList.forEach(eb => eb.time = val); |
| saveData(); |
| renderConfigEditor(); |
| updateUI(); |
| showToast("Quick Set Applied!"); |
| } |
| } |
| |
| function addEB(cupIdx) { db.config.cups[cupIdx].ebList.push({ label: 'New Early Bird', time: '' }); renderConfigEditor(); } |
| function removeEB(cupIdx, ebIdx) { db.config.cups[cupIdx].ebList.splice(ebIdx, 1); renderConfigEditor(); } |
| |
| function updateCasinoConfig() { |
| db.config.casino.nextSpin = document.getElementById('confNextSpin').value; |
| db.config.casino.customStatus = document.getElementById('confStatusMsg').value; |
| db.config.casino.lastWinner.name = document.getElementById('confWinnerName').value; |
| db.config.casino.lastWinner.amount = document.getElementById('confWinnerAmount').value; |
| saveData(); |
| updateUI(); |
| } |
| |
| function setMasterDeadline() { |
| const masterDate = document.getElementById('masterDeadlineInput').value; |
| if(!masterDate) return alert("Pick a date first."); |
| if(confirm(`Set ALL 4 bucket deadlines to ${masterDate}?`)) { |
| db.config.cups.forEach(c => c.deadline = masterDate); |
| saveData(); |
| updateUI(); |
| showToast("All Deadlines Updated!"); |
| } |
| } |
| |
| function generateAuditReport() { |
| const gross = (db.verifiedPool + db.casinoPot + (db.adminFee || 0)).toFixed(2); |
| const net = db.verifiedPool.toFixed(2); |
| const roundGoal = 315; |
| const currentRound = Math.floor(db.verifiedPool / roundGoal) + 1; |
| let assetDetails = db.assets.map(a => `• ${a.game}: Won ${a.winDate} | ${a.status}`).join('\n'); |
| let winDetails = db.winnings.map(w => `• ${w.source}: $${w.amount}`).join('\n'); |
| let memberStandings = [...db.members].sort((a,b) => b.totalWon - a.totalWon).map(m => `• ${m.display}: ${m.shares.toFixed(1)} Shares | Won: $${m.totalWon.toFixed(2)}`).join('\n'); |
| const report = `AUDIT REPORT\nTotal Gross: $${gross}\nNet Liquidity: $${net}\nCasino Pot: $${db.casinoPot.toFixed(2)}\nAdmin Fees: $${(db.adminFee || 0).toFixed(2)}\nRound: ${currentRound}\n\n${memberStandings}`; |
| document.getElementById('modalTitle').innerText = "Monthly Audit Report"; |
| document.getElementById('modalBody').innerText = report; |
| document.getElementById('reportModal').style.display = 'block'; |
| } |
| |
| function verifyParticipant() { |
| const nInput = document.getElementById('lName'); |
| const eInput = document.getElementById('lEmail'); |
| const aInput = document.getElementById('lAmount'); |
| const tInput = document.getElementById('lManualTime'); |
| const refInput = document.getElementById('lRef'); |
| if (!nInput.value || !aInput.value) return alert("Required Info Missing."); |
| |
| const timeObj = tInput.value ? new Date(tInput.value) : new Date(); |
| const rawAmount = parseFloat(aInput.value); |
| const feeAdmin = 0.50; |
| const feeCasino = 0.50; |
| const netShares = rawAmount - feeAdmin - feeCasino; |
| |
| if (netShares < 0) return alert("Error: Amount too low."); |
| |
| if (db.adminFee === undefined) db.adminFee = 0; |
| db.adminFee += feeAdmin; |
| db.casinoPot += feeCasino; |
| |
| db.members.push({ |
| display: nInput.value, email: eInput.value.toLowerCase(), shares: netShares, |
| paidAt: timeObj.toLocaleString('en-CA', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }), |
| rawTime: timeObj.getTime(), isFreeTicket: false, ftSource: '', totalWon: 0, |
| refId: refInput.value || 'N/A' |
| }); |
| |
| nInput.value = ""; eInput.value = ""; aInput.value = ""; tInput.value = ""; refInput.value = ""; |
| saveData(); updateUI(); showToast("Verified & Saved!"); |
| } |
| |
| function addAsset() { |
| const game = document.getElementById('assetGame').value; |
| const date = document.getElementById('assetDate').value; |
| if(!date) return alert("Pick Win Date"); |
| db.assets.push({ id: Date.now(), game, winDate: date, status: 'Active', usedDate: '', snapshot: [] }); |
| saveData(); updateUI(); showToast("Asset Recorded!"); |
| } |
| |
| function markAssetUsed(id) { |
| if(!confirm("Lock Snapshot?")) return; |
| const asset = db.assets.find(a => a.id === id); |
| if(asset) { |
| asset.status = 'Used'; |
| asset.usedDate = new Date().toLocaleDateString(); |
| const cutoffDate = new Date(db.config.cups[3].deadline).getTime(); |
| const activeMembers = db.members.filter(m => m.rawTime <= cutoffDate); |
| asset.snapshot = JSON.parse(JSON.stringify(activeMembers)); |
| saveData(); updateUI(); showToast("Snapshot Locked!"); |
| } |
| } |
| |
| function deleteWin(idx) { |
| if(!confirm("Delete Win?")) return; |
| const win = db.winnings[idx]; |
| win.breakdowns.forEach(item => { |
| let mem = db.members.find(m => m.display === item.name); |
| if(mem) { mem.totalWon -= parseFloat(item.amount); if(mem.totalWon < 0) mem.totalWon = 0; } |
| }); |
| db.winnings.splice(idx, 1); |
| saveData(); updateUI(); showToast("Reversed!"); |
| } |
| |
| function toggleWinSource() { |
| const type = document.getElementById('winType').value; |
| const select = document.getElementById('winSource'); |
| let options = ""; |
| if(type === 'standard') { |
| options = db.config.cups.map(c => `<option value="${c.name}">${c.name}</option>`).join('') + |
| `<option value="Lotto Max">Lotto Max</option><option value="Lotto 649">Lotto 649</option><option value="Daily Grand">Daily Grand</option><option value="Ontario 49">Ontario 49</option>`; |
| } else if(type === 'eb') { |
| db.config.cups.forEach(c => { c.ebList.forEach(eb => { options += `<option value="EB: ${c.name} - ${eb.label}">EB: ${c.name} (${eb.label})</option>`; }); }); |
| } else if(type === 'asset') { |
| const usedAssets = db.assets.filter(a => a.status === 'Used'); |
| if(usedAssets.length === 0) options = `<option value="">No Used Assets</option>`; |
| else options = usedAssets.map(a => `<option value="Asset_${a.id}">Used ${a.game} (${a.usedDate})</option>`).join(''); |
| } |
| select.innerHTML = options; |
| } |
| |
| function calculatePayouts() { |
| const type = document.getElementById('winType').value; |
| const sourceVal = document.getElementById('winSource').value; |
| const rawVal = document.getElementById('lPrizeAmount').value; |
| const pVal = parseFloat(rawVal); |
| if (rawVal === "" || isNaN(pVal) || pVal < 0) return alert("Enter prize amount."); |
| |
| let targetGroup = []; |
| let displaySource = sourceVal; |
| |
| if(type === 'asset' && sourceVal.startsWith('Asset_')) { |
| const assetId = parseInt(sourceVal.split('_')[1]); |
| const asset = db.assets.find(a => a.id === assetId); |
| targetGroup = asset.snapshot; |
| displaySource = `Restricted Win: ${asset.game} (From Used Asset)`; |
| } else { |
| const cutoffDate = new Date(db.config.cups[3].deadline).getTime(); |
| targetGroup = db.members.filter(m => m.rawTime <= cutoffDate); |
| } |
| |
| const totalGroupShares = targetGroup.reduce((s, m) => s + m.shares, 0); |
| const breakdowns = targetGroup.map(m => { |
| const cut = totalGroupShares > 0 ? (pVal * (m.shares / totalGroupShares)) : 0; |
| const realMember = db.members.find(rm => rm.email === m.email); |
| if(realMember) realMember.totalWon += cut; |
| return { name: m.display, amount: cut.toFixed(2) }; |
| }); |
| |
| db.winnings.push({ source: displaySource, amount: pVal.toFixed(2), date: new Date().toLocaleDateString(), breakdowns }); |
| currentPayoutIdx = db.winnings.length - 1; |
| saveData(); renderPayoutLogic(); updateUI(); |
| } |
| |
| function renderPayoutLogic() { |
| const container = document.getElementById('payoutResults'); |
| if(db.winnings.length === 0) { |
| container.innerHTML = '<small style="color:#666;">No payouts recorded yet.</small>'; |
| document.getElementById('payoutTitle').innerText = "History (0)"; |
| return; |
| } |
| |
| if(currentPayoutIdx < 0) currentPayoutIdx = db.winnings.length - 1; |
| const win = db.winnings[currentPayoutIdx]; |
| |
| document.getElementById('payoutTitle').innerText = `Payout ${currentPayoutIdx + 1} / ${db.winnings.length}`; |
| |
| let html = ` |
| <div style="background:#e8f8f5; padding:15px; border-radius:8px; border:1px solid #2ecc71;"> |
| <div style="display:flex; justify-content:space-between; margin-bottom:10px; border-bottom:1px solid #ccc; padding-bottom:5px;"> |
| <strong style="color:#145a32;">${win.source}</strong> |
| <strong style="font-size:1.2em;">$${win.amount}</strong> |
| </div> |
| <div style="max-height:150px; overflow-y:auto; font-size:0.9em;"> |
| <table style="margin:0;"> |
| ${win.breakdowns.map(b => `<tr><td>${b.name}</td><td style="text-align:right;">$${b.amount}</td></tr>`).join('')} |
| </table> |
| </div> |
| <div style="text-align:right; font-size:0.7em; color:#666; margin-top:5px;">${win.date}</div> |
| </div> |
| `; |
| container.innerHTML = html; |
| } |
| |
| function spinWheel() { |
| if (btoa(prompt("Enter Leader Password:")) !== LEADER_KEY_ENCODED) return alert("Access Denied"); |
| |
| const cutoffDate = new Date(db.config.cups[3].deadline).getTime(); |
| const activeMembers = db.members.filter(m => m.rawTime <= cutoffDate); |
| |
| if (activeMembers.length === 0) return alert("No active members to spin for!"); |
| |
| const visiblePotText = document.getElementById('casinoBalance').innerText.replace('$',''); |
| const potValue = parseFloat(visiblePotText); |
| |
| if (potValue <= 0) return alert("The Pot is empty!"); |
| |
| if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); |
| |
| const activeTotalShares = activeMembers.reduce((s, m) => s + m.shares, 0); |
| |
| let random = Math.random() * activeTotalShares; |
| let winner = null; |
| let currentWeight = 0; |
| let startAngle = 0; |
| let endAngle = 0; |
| |
| for (let m of activeMembers.sort((a,b) => b.shares - a.shares)) { |
| let weight = m.shares; |
| let nextWeight = currentWeight + weight; |
| if (random >= currentWeight && random < nextWeight) { |
| winner = m; |
| startAngle = (currentWeight / activeTotalShares) * 360; |
| endAngle = (nextWeight / activeTotalShares) * 360; |
| break; |
| } |
| currentWeight += weight; |
| } |
| if (!winner) winner = activeMembers[0]; |
| |
| const centerAngle = (startAngle + endAngle) / 2; |
| const spins = 10 * 360; |
| const targetRotation = spins + (360 - centerAngle); |
| const wheel = document.getElementById('casinoWheel'); |
| |
| wheel.style.transition = "transform 10s cubic-bezier(0.1, 0.9, 0.1, 1.0)"; |
| wheel.style.transform = `rotate(${targetRotation}deg)`; |
| |
| let clickInterval = 100; |
| let clicks = 0; |
| const clicker = setInterval(() => { |
| clicks++; |
| if(clicks > 80) clearInterval(clicker); |
| else { |
| playClickSound(); |
| clickInterval += (clicks * 2); |
| } |
| }, 100); |
| |
| setTimeout(() => { |
| clearInterval(clicker); |
| playWinSound(); |
| |
| latestWinner = winner.display; |
| document.getElementById('winnerName').innerText = winner.display; |
| document.getElementById('winnerAmount').innerText = "$" + potValue.toFixed(2); |
| document.getElementById('winnerOverlay').style.display = "flex"; |
| |
| db.winnings.push({ |
| source: '🎰 Casino Pot Jackpot', |
| amount: potValue.toFixed(2), |
| date: new Date().toLocaleDateString(), |
| breakdowns: [{ name: winner.display, amount: potValue.toFixed(2) }] |
| }); |
| |
| const userIndex = db.members.findIndex(m => m.email === winner.email); |
| if (userIndex !== -1) { |
| db.members[userIndex].totalWon += potValue; |
| } |
| |
| db.config.casino.lastWinner = { name: winner.display, amount: potValue.toFixed(2) }; |
| |
| db.casinoPot = db.casinoPot - potValue; |
| if(db.casinoPot < 0) db.casinoPot = 0; |
| |
| saveData(); |
| updateUI(); |
| |
| setTimeout(() => { |
| wheel.style.transition = "none"; |
| wheel.style.transform = "rotate(0deg)"; |
| }, 1000); |
| |
| for(let i=0; i<50; i++) { |
| let c = document.createElement('div'); |
| c.className = 'confetti'; |
| c.style.left = Math.random() * 100 + 'vw'; |
| c.style.animationDuration = (Math.random() * 3 + 2) + 's'; |
| c.style.backgroundColor = ['#f00','#0f0','#00f','#ff0','#0ff'][Math.floor(Math.random()*5)]; |
| document.getElementById('winnerOverlay').appendChild(c); |
| } |
| |
| }, 10500); |
| } |
| |
| function closeWinnerOverlay() { |
| document.getElementById('winnerOverlay').style.display = "none"; |
| const confettis = document.querySelectorAll('.confetti'); |
| confettis.forEach(c => c.remove()); |
| updateUI(); |
| } |
| |
| function renderCasinoPie(activeMembers) { |
| const container = document.getElementById('casinoWheel'); |
| const legend = document.getElementById('wheelLegend'); |
| const wheelContainer = document.getElementById('casinoWheelContainer'); |
| const emptyState = document.getElementById('casinoEmptyState'); |
| const lastWinDisplay = document.getElementById('lastWinnerDisplay'); |
| const lastWinName = document.getElementById('lastWinnerName'); |
| const nextSpinDisplay = document.getElementById('nextSpinDisplay'); |
| const casinoStatus = document.getElementById('casinoStatus'); |
| |
| if (db.config.casino.nextSpin) { |
| const spinDate = new Date(db.config.casino.nextSpin); |
| const now = new Date(); |
| const diffTime = spinDate - now; |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); |
| |
| if (diffDays > 0) { |
| nextSpinDisplay.innerHTML = `⏳ NEXT SPIN: ${spinDate.toLocaleDateString()} (${diffDays} Days)`; |
| nextSpinDisplay.style.display = "block"; |
| } else if (diffDays <= 0) { |
| nextSpinDisplay.innerHTML = `⚠️ SPINNING TODAY!`; |
| nextSpinDisplay.style.display = "block"; |
| } |
| } else { |
| nextSpinDisplay.style.display = "none"; |
| } |
| |
| if (db.config.casino.customStatus) { |
| casinoStatus.innerText = "💥 " + db.config.casino.customStatus + " 💥"; |
| } else { |
| casinoStatus.innerText = "💥 One lucky winner takes the whole pot! Is it you? 💥"; |
| } |
| |
| const winnerData = db.config.casino.lastWinner; |
| if (winnerData && winnerData.name) { |
| lastWinDisplay.style.display = 'block'; |
| lastWinName.innerText = `${winnerData.name} ($${winnerData.amount})`; |
| document.getElementById('confWinnerName').value = winnerData.name; |
| document.getElementById('confWinnerAmount').value = winnerData.amount; |
| } else { |
| lastWinDisplay.style.display = 'none'; |
| } |
| |
| if (!activeMembers || activeMembers.length === 0) { |
| wheelContainer.style.display = 'none'; |
| emptyState.style.display = 'block'; |
| return; |
| } |
| |
| const visiblePotText = document.getElementById('casinoBalance').innerText.replace('$',''); |
| if (parseFloat(visiblePotText) <= 0) { |
| wheelContainer.style.display = 'none'; |
| emptyState.style.display = 'block'; |
| return; |
| } |
| |
| wheelContainer.style.display = 'block'; |
| emptyState.style.display = 'none'; |
| |
| const sorted = [...activeMembers].sort((a,b) => b.shares - a.shares); |
| const totalShares = sorted.reduce((s,m)=>s+m.shares,0) || 1; |
| |
| let gradientStr = ""; |
| let startDeg = 0; |
| let legendHtml = ""; |
| |
| const top5 = sorted.slice(0, 5); |
| |
| sorted.forEach((m, idx) => { |
| const percent = (m.shares / totalShares) * 100; |
| const color = casinoColors[idx % casinoColors.length]; |
| const endDeg = startDeg + percent; |
| |
| gradientStr += `${color} ${startDeg}% ${endDeg}%, `; |
| startDeg = endDeg; |
| |
| if (idx < 5) { |
| legendHtml += ` |
| <div class="legend-item"> |
| <span><span class="legend-color" style="background:${color}"></span>${m.display.split(' ')[0]}</span> |
| <strong>${percent.toFixed(1)}%</strong> |
| </div> |
| `; |
| } |
| }); |
| |
| if (sorted.length > 5) { |
| legendHtml += `<div class="legend-item" style="color:#666; font-style:italic;"><span>+ ${sorted.length - 5} Others</span></div>`; |
| } |
| |
| gradientStr = gradientStr.slice(0, -2); |
| container.style.background = `conic-gradient(${gradientStr})`; |
| legend.innerHTML = legendHtml; |
| } |
| |
| function updateUI() { |
| renderConfigEditor(); |
| |
| const cutoffDate = new Date(db.config.cups[3].deadline).getTime(); |
| |
| const activeMembers = db.members.filter(m => m.rawTime <= cutoffDate); |
| const futureMembers = db.members.filter(m => m.rawTime > cutoffDate); |
| |
| const activeLiquidity = activeMembers.reduce((sum, m) => sum + m.shares, 0); |
| |
| const pendingCasinoRevenue = futureMembers.length * 0.50; |
| const visibleCasinoPot = Math.max(0, db.casinoPot - pendingCasinoRevenue); |
| |
| const activeGross = activeLiquidity + visibleCasinoPot; |
| |
| document.getElementById('grossTotal').innerHTML = `$${activeGross.toFixed(2)} <div style="font-size:0.7em; font-weight:normal; color:#666;">($${activeLiquidity.toFixed(2)} Liquidity + $${visibleCasinoPot.toFixed(2)} Casino 🎲) <span style="color:var(--success); font-weight:bold;">✨ Win the Pot! ✨</span></div>`; |
| document.getElementById('totalPool').innerText = "$" + activeLiquidity.toFixed(2); |
| document.getElementById('totalShares').innerText = activeLiquidity.toFixed(1); |
| |
| document.getElementById('casinoBalance').innerText = "$" + visibleCasinoPot.toFixed(2); |
| document.getElementById('adminWallet').innerText = "$" + (db.adminFee || 0).toFixed(2); |
| |
| if (db.config.casino) { |
| document.getElementById('confNextSpin').value = db.config.casino.nextSpin || ""; |
| document.getElementById('confStatusMsg').value = db.config.casino.customStatus || ""; |
| } |
| |
| const roundGoal = 315; |
| const currentRound = Math.floor(activeLiquidity / roundGoal) + 1; |
| let currentLiquidity = activeLiquidity % roundGoal || (activeLiquidity > 0 ? roundGoal : 0); |
| |
| document.getElementById('roundStatus').innerText = "ROUND " + currentRound + " ACTIVE"; |
| document.getElementById('roundProgressLabel').innerText = `Round ${currentRound}: $${currentLiquidity.toFixed(2)} / $${roundGoal.toFixed(2)}`; |
| document.getElementById('globalProgress').style.width = ((currentLiquidity / roundGoal) * 100) + "%"; |
| |
| let cupHtml = ""; let cupOffset = 0; |
| db.config.cups.forEach(cup => { |
| let val = Math.min(Math.max(currentLiquidity - cupOffset, 0), cup.max); |
| const tArr = cup.tiers.split(',').map(Number); |
| const lArr = cup.tickets.split(','); |
| let nextT = tArr.find(t => t > val); |
| let encouragement = val >= cup.max ? "🚀 Tier Locked!" : (nextT ? `✨ Only $${(nextT - val).toFixed(2)} more for ${lArr[tArr.indexOf(nextT)]}!` : "Flowing..."); |
| let markers = tArr.map((t, i) => `<div class="tier-marker" style="bottom: ${(t/cup.max)*100}%">$${t}: ${lArr[i]}</div>`).join(''); |
| |
| let ebItemsHtml = ""; |
| if (cup.id === 'olg') { |
| ebItemsHtml = ` |
| <div style="font-weight:bold; color:var(--primary); margin-bottom:5px; border-bottom:1px solid #eee; padding-bottom:3px;">$40 MAX PACK INCLUDES:</div> |
| <div class="eb-item"><span>3x Lotto Max + Encore</span></div> |
| <div class="eb-item"><span>3x Lotto 6/49 + Encore</span></div> |
| <div class="eb-item"><span>3x Daily Grand + Encore</span></div> |
| <div class="eb-item"><span>3x Ontario 49 + Encore</span></div> |
| `; |
| } else { |
| cup.ebList.forEach(eb => { |
| const isEnded = new Date().getTime() > new Date(eb.time).getTime(); |
| ebItemsHtml += `<div class="eb-item ${isEnded ? 'eb-ended' : 'eb-active'}"><span>${eb.label}</span><span>${getDaysLeft(eb.time)}</span></div>`; |
| }); |
| } |
| |
| let cycleHtml = `<div class="cycle-info"><div>Funding Open:<br><span class="cycle-date">${cup.startDate || 'TBA'}</span></div><div style="text-align:right;">Grand Draw:<br><span class="cycle-date">${cup.deadline || 'TBA'}</span></div></div>`; |
| |
| cupHtml += `<div class="card"><div class="measuring-cup-wrapper"><div class="cup-container"><div class="cup-fill" style="height: ${(val/cup.max)*100}%"></div><div class="cup-label">${cup.name}</div>${markers}</div></div><div class="encouragement ${!val && 'shortfall'}">${encouragement}</div>${cycleHtml}<div class="eb-list">${ebItemsHtml}</div></div>`; |
| cupOffset += cup.max; |
| }); |
| document.getElementById('cupDisplay').innerHTML = cupHtml; |
| |
| const sorted = [...activeMembers].sort((a,b) => b.shares - a.shares); |
| document.getElementById('equityBody').innerHTML = sorted.map((m, i) => { |
| let badges = ""; |
| db.config.cups.forEach((c, idx) => { |
| const prevCupSum = db.config.cups.slice(0,idx).reduce((s,x)=>s+x.max,0); |
| const cupFunded = Math.min(Math.max(currentLiquidity - prevCupSum, 0), c.max) > 0; |
| c.ebList.forEach(eb => { if(m.rawTime < new Date(eb.time).getTime() && cupFunded) badges += `<span class="badge bg-eb">${eb.label}</span>`; }); |
| }); |
| const ftBadge = m.isFreeTicket ? `<span class="badge bg-ft">${m.ftSource ? m.ftSource + ' FT' : 'Free Ticket'}</span>` : ""; |
| |
| let rowClass = ""; |
| if (latestWinner === m.display) rowClass = "winner-row"; |
| |
| return `<tr class="${rowClass}"><td>#${i+1}</td><td>${m.display}<br><small style="color:#666; font-size:0.8em;">Ref: ${m.refId || '-'}</small></td><td>${m.shares.toFixed(1)}</td><td style="color:var(--success); font-weight:bold;">$${m.totalWon.toFixed(2)}</td><td>${activeLiquidity > 0 ? ((m.shares/activeLiquidity)*100).toFixed(2) : 0}%</td><td>${badges} ${ftBadge} <span class="badge bg-verified">Verified</span></td></tr>`; |
| }).join(''); |
| |
| if(futureMembers.length > 0) { |
| document.getElementById('holdingTankTable').innerHTML = `<table><thead><tr><th>Pending Member</th><th>Date Paid</th><th>Status</th></tr></thead><tbody>` + |
| futureMembers.map(m => `<tr><td>${m.display}</td><td>${m.paidAt}</td><td><span class="badge bg-rollover">⏳ On Deck</span></td></tr>`).join('') + `</tbody></table>`; |
| } else { |
| document.getElementById('holdingTankTable').innerHTML = `<small style="color:#666;">No future members pending.</small>`; |
| } |
| |
| document.getElementById('leaderAssetVaultTable').innerHTML = `<table><thead><tr><th>Game</th><th>Win Date</th><th>Status</th><th>Used Date</th><th>Snapshot</th></tr></thead><tbody>` + |
| db.assets.map(a => { |
| const nameList = a.snapshot.map(m => m.display).join(', '); |
| const snapshotDisplay = a.status === 'Used' ? `<details><summary style="cursor:pointer; color:var(--primary); font-weight:bold;">${a.snapshot.length} Eligible +</summary><div style="font-size:0.85em; background:#eef2f7; padding:8px; border-radius:6px; margin-top:5px; border:1px solid #d1d9e6;"><strong>Members Locked:</strong><br>${nameList}</div></details>` : `<button class="btn btn-asset" onclick="markAssetUsed(${a.id})">Mark Used</button>`; |
| return `<tr><td>${a.game}</td><td>${a.winDate}</td><td><span class="badge ${a.status==='Active'?'bg-verified':'bg-eb'}">${a.status}</span></td><td>${a.usedDate || '-'}</td><td>${snapshotDisplay}</td></tr>`; |
| }).join('') + `</tbody></table>`; |
| |
| document.getElementById('assetVaultTable').innerHTML = `<table><thead><tr><th>Game</th><th>Win Date</th><th>Status</th></tr></thead><tbody>` + |
| db.assets.map(a => `<tr><td>${a.game}</td><td>${a.winDate}</td><td><span class="badge ${a.status==='Active'?'bg-verified':'bg-eb'}">${a.status}</span></td></tr>`).join('') + `</tbody></table>`; |
| |
| if (db.winnings.length > 0) { |
| document.getElementById('winHistory').innerHTML = `<table><thead><tr><th>Source</th><th>Amount</th><th>Date</th></tr></thead><tbody>` + db.winnings.map(w => `<tr><td>${w.source}</td><td style="color:var(--success); font-weight:bold;">$${w.amount}</td><td>${w.date}</td></tr>`).join('') + `</tbody></table>`; |
| document.getElementById('adminWinHistory').innerHTML = `<table><thead><tr><th>Source</th><th>Amount</th><th>Date</th><th>Action</th></tr></thead><tbody>` + db.winnings.map((w, i) => `<tr><td>${w.source}</td><td style="color:var(--success); font-weight:bold;">$${w.amount}</td><td>${w.date}</td><td><button class="btn btn-danger" style="font-size:0.7em; padding:2px 5px;" onclick="deleteWin(${i})">Delete</button></td></tr>`).join('') + `</tbody></table>`; |
| } else { |
| document.getElementById('winHistory').innerHTML = `<p><small>No results recorded for this month yet.</small></p>`; |
| document.getElementById('adminWinHistory').innerHTML = "<small>No winnings to manage.</small>"; |
| } |
| |
| toggleWinSource(); |
| |
| const adminList = [...db.members].sort((a,b) => { |
| let valA = a[sortConfig.key]; |
| let valB = b[sortConfig.key]; |
| if (typeof valA === 'string') valA = valA.toLowerCase(); |
| if (typeof valB === 'string') valB = valB.toLowerCase(); |
| if (valA < valB) return sortConfig.asc ? -1 : 1; |
| if (valA > valB) return sortConfig.asc ? 1 : -1; |
| return 0; |
| }); |
| |
| document.getElementById('adminMemberBody').innerHTML = adminList.map((m, idx) => { |
| const realIdx = db.members.findIndex(x => x.email === m.email && x.rawTime === m.rawTime); |
| const isFuture = m.rawTime > cutoffDate; |
| const rowStyle = isFuture ? 'background:#eef2f7; color:#666;' : ''; |
| const badge = isFuture ? '<span class="badge bg-rollover">⏳ Future</span>' : '<span class="badge bg-verified">Active</span>'; |
| |
| return `<tr style="${rowStyle}"><td>${m.display}</td><td>${m.email}</td><td>${m.refId || '-'}</td><td>${m.paidAt}</td><td>${(m.shares + 1.00).toFixed(2)}</td><td>${badge}</td><td><select onchange="db.members[${realIdx}].ftSource = this.value; saveData(); updateUI()" ${!m.isFreeTicket ? 'disabled' : ''}><option value="">Select Asset Source</option><option value="Lotto Max" ${m.ftSource=='Lotto Max'?'selected':''}>Lotto Max</option><option value="Lotto 649" ${m.ftSource=='Lotto 649'?'selected':''}>Lotto 649</option><option value="Daily Grand" ${m.ftSource=='Daily Grand'?'selected':''}>Daily Grand</option><option value="Ontario 49" ${m.ftSource=='Ontario 49'?'selected':''}>Ontario 49</option></select></td><td><button class="btn btn-primary" onclick="toggleFreeTicket(${realIdx})">Toggle FT</button> <button class="btn btn-danger" onclick="removeMember(${realIdx})">Delete</button></td></tr>`; |
| }).join(''); |
| |
| renderCasinoPie(activeMembers); |
| } |
| |
| function changePayoutView(dir) { |
| if (db.winnings.length === 0) return; |
| currentPayoutIdx = Math.max(0, Math.min(db.winnings.length - 1, currentPayoutIdx + dir)); |
| renderPayoutLogic(); |
| } |
| |
| function withdrawCasino() { if (confirm("Withdraw strategic pot?")) { db.casinoPot = 0; saveData(); updateUI(); } } |
| function withdrawAdmin() { if (confirm("Withdraw Admin Fees?")) { db.adminFee = 0; saveData(); updateUI(); } } |
| function toggleFreeTicket(idx) { db.members[idx].isFreeTicket = !db.members[idx].isFreeTicket; if(!db.members[idx].isFreeTicket) db.members[idx].ftSource = ''; saveData(); updateUI(); } |
| function removeMember(idx) { if (confirm("Delete member?")) { const m = db.members[idx]; db.verifiedPool -= m.shares; db.totalShares -= m.shares; db.casinoPot -= 0.50; db.adminFee -= 0.50; db.members.splice(idx,1); saveData(); updateUI(); } } |
| |
| function factoryReset() { |
| if (confirm("⚠️ FACTORY RESET: ARE YOU SURE? ⚠️\n\nThis will delete ALL members, winnings, and assets.\nThere is no undo.")) { |
| if(prompt("Type 'DELETE' to confirm:") === "DELETE") { |
| db.members = []; |
| db.verifiedPool = 0; |
| db.casinoPot = 0; |
| db.adminFee = 0; |
| db.totalShares = 0; |
| db.winnings = []; |
| db.assets = []; |
| db.lastCasinoWinner = null; |
| db.config.casino.lastWinner = { name: "", amount: 0 }; |
| saveData(); |
| updateUI(); |
| alert("System Wiped."); |
| } |
| } |
| } |
| |
| function resetPool() { |
| const cutoffStr = db.config.cups[3].deadline; |
| const cutoffTime = new Date(cutoffStr).getTime(); |
| if (!confirm(`⚠️ START NEW MONTH PROTOCOL ⚠️\n\nCut-Off Date: ${cutoffStr}\n\n1. This will DOWNLOAD a backup of the current month.\n2. Active members (Before Deadline) will be ARCHIVED.\n3. Future members (After Deadline) will become ACTIVE.\n\nAre you sure?`)) return; |
| exportDB(); |
| const futureMembers = db.members.filter(m => m.rawTime > cutoffTime); |
| |
| db.members = futureMembers; |
| |
| const newLiquidity = futureMembers.reduce((sum, m) => sum + m.shares, 0); |
| const keptCasinoFees = futureMembers.length * 0.50; |
| |
| db.verifiedPool = newLiquidity; |
| db.totalShares = newLiquidity; |
| db.casinoPot = keptCasinoFees; |
| |
| db.winnings = []; |
| db.assets = []; |
| db.lastCasinoWinner = null; |
| db.config.casino.lastWinner = { name: "", amount: 0 }; |
| |
| saveData().then(() => { alert(`✅ New Month Started!\n\nRolled Over: ${futureMembers.length} Members\nStarting Pool: $${newLiquidity.toFixed(2)}`); location.reload(); }); |
| } |
| |
| function exportDB() { const blob = new Blob([JSON.stringify(db)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `northern_portfolio_backup_${new Date().toISOString().slice(0,10)}.json`; a.click(); } |
| function importDB(ev) { const reader = new FileReader(); reader.onload = () => { try { db = JSON.parse(reader.result); saveData(); updateUI(); showToast("Loaded!"); } catch(e) { alert("Error."); } }; reader.readAsText(ev.target.files[0]); } |
| |
| window.onload = loadData; |
| updateUI(); |
| </script> |
| </body> |
| </html> |