Spaces:
Sleeping
Sleeping
| {% extends 'base.html' %} | |
| {% block title %}Admin – {{ app_brand }}{% endblock %} | |
| {% block head %} | |
| <style> | |
| @media (max-width: 860px) { | |
| .admin-result-form { | |
| min-width: 0 ; | |
| width: 100%; | |
| } | |
| .admin-header-logout { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="page"> | |
| <div class="page-header" style="display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:space-between; gap:1rem;"> | |
| <div> | |
| <div class="page-title">⚙️ ADMIN PANEL</div> | |
| <div class="page-subtitle">Manage matches, users, results and points</div> | |
| </div> | |
| <a href="{{ url_for('admin_logout') }}" class="btn btn-ghost btn-sm admin-header-logout" style="align-self:center;">🔒 Log out admin</a> | |
| </div> | |
| <!-- Tab Nav --> | |
| <div style="display:flex; gap:0.5rem; margin-bottom:1.5rem; border-bottom:1px solid var(--border); padding-bottom:0.5rem; flex-wrap:wrap;"> | |
| {% for tab in [('matches','📅 Matches'),('add_match','➕ Add Match'),('results','✅ Set Results'),('users','👥 Users')] %} | |
| <button class="btn btn-ghost btn-sm tab-btn" data-tab="{{ tab[0] }}" onclick="showTab('{{ tab[0] }}')">{{ tab[1] }}</button> | |
| {% endfor %} | |
| </div> | |
| <!-- ── TAB: MATCHES ─────────────────────────────────────────────────────── --> | |
| <div id="tab-matches" class="tab-panel"> | |
| <div style="display:flex; justify-content:flex-end; margin-bottom:0.75rem;"> | |
| <a href="{{ url_for('team_pool') }}" class="btn btn-secondary btn-sm">🃏 Open team Pool (everyone’s picks)</a> | |
| </div> | |
| <div class="card"> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>#</th><th>Teams</th><th>Date & Time</th><th>Venue</th> | |
| <th>Status</th><th>Winner</th><th>Preds</th><th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for match in matches %} | |
| <tr> | |
| <td style="color:var(--muted);">{{ match.match_number or '—' }}</td> | |
| <td> | |
| <div style="font-weight:700;"> | |
| <span style="color:{{ match.team1_color }};">{{ match.team1_abbr }}</span> | |
| <span style="color:var(--muted);"> vs </span> | |
| <span style="color:{{ match.team2_color }};">{{ match.team2_abbr }}</span> | |
| </div> | |
| <div style="font-size:0.75rem; color:var(--muted2);">{{ match.team1 }} vs {{ match.team2 }}</div> | |
| </td> | |
| <td style="font-size:0.85rem; white-space:nowrap;"> | |
| {{ match.match_date|format_date }}<br> | |
| <span style="color:var(--muted2);">{{ match.match_time }}</span> | |
| </td> | |
| <td style="font-size:0.8rem; color:var(--muted2);">{{ match.venue or '—' }}</td> | |
| <td> | |
| <span class="badge badge-{{ match.status }}">{{ match.status }}</span> | |
| </td> | |
| <td style="font-size:0.85rem;"> | |
| {% if match.winner and match.winner != 'ABANDONED' %} | |
| <div style="color:var(--green);">{{ match.winner }}</div> | |
| {% if match.man_of_match %}<div style="color:var(--muted2); font-size:0.78rem;">⭐ {{ match.man_of_match }}</div>{% endif %} | |
| {% elif match.status == 'abandoned' %}<span style="color:var(--muted);">Abandoned</span> | |
| {% else %}—{% endif %} | |
| </td> | |
| <td> | |
| <a href="{{ url_for('admin_match_predictions', match_id=match.id) }}" class="btn btn-ghost btn-sm">View</a> | |
| </td> | |
| <td> | |
| <div style="display:flex; gap:0.4rem; flex-wrap:wrap;"> | |
| <!-- Status update --> | |
| <form method="post" action="{{ url_for('admin_update_status', match_id=match.id) }}" style="display:flex; gap:0.3rem;"> | |
| <select name="status" style="width:auto; font-size:0.78rem; padding:0.3rem 0.5rem;"> | |
| {% for s in statuses %} | |
| <option value="{{ s }}" {% if match.status == s %}selected{% endif %}>{{ s }}</option> | |
| {% endfor %} | |
| </select> | |
| <button type="submit" class="btn btn-secondary btn-sm" style="padding:0.3rem 0.5rem;">Set</button> | |
| </form> | |
| <!-- Delete --> | |
| <form method="post" action="{{ url_for('admin_delete_match', match_id=match.id) }}" | |
| onsubmit="return confirm('Delete this match? (Only possible if no predictions exist)')"> | |
| <button type="submit" class="btn btn-danger btn-sm" style="padding:0.3rem 0.5rem;">🗑</button> | |
| </form> | |
| </div> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── TAB: ADD MATCH ───────────────────────────────────────────────────── --> | |
| <div id="tab-add_match" class="tab-panel" style="display:none;"> | |
| <div class="card" style="max-width:700px;"> | |
| <div class="card-title">➕ ADD NEW MATCH</div> | |
| <form method="post" action="{{ url_for('admin_add_match') }}"> | |
| <div class="form-row cols-2"> | |
| <div class="form-group"> | |
| <label>Team 1 *</label> | |
| <select name="team1" required> | |
| <option value="">Select team…</option> | |
| {% for t in teams %}<option value="{{ t }}">{{ t }}</option>{% endfor %} | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Team 2 *</label> | |
| <select name="team2" required> | |
| <option value="">Select team…</option> | |
| {% for t in teams %}<option value="{{ t }}">{{ t }}</option>{% endfor %} | |
| </select> | |
| </div> | |
| </div> | |
| <div class="form-row cols-3"> | |
| <div class="form-group"> | |
| <label>Match Date *</label> | |
| <input type="date" name="match_date" required value="{{ today }}"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Start Time *</label> | |
| <input type="time" name="match_time" required value="19:30"> | |
| <div class="form-hint">{% if points_config.lock_minutes_before %}Lock auto-triggers {{ points_config.lock_minutes_before }} min before start{% else %}Lock auto-triggers at scheduled match start{% endif %}</div> | |
| </div> | |
| <div class="form-group"> | |
| <label>Match Number</label> | |
| <input type="number" name="match_number" placeholder="e.g. 1"> | |
| </div> | |
| </div> | |
| <div class="form-row cols-2"> | |
| <div class="form-group"> | |
| <label>Venue</label> | |
| <input type="text" name="venue" placeholder="e.g. Wankhede Stadium"> | |
| </div> | |
| <div class="form-group"> | |
| <label>City</label> | |
| <input type="text" name="city" placeholder="e.g. Mumbai"> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary">➕ Add Match</button> | |
| </form> | |
| </div> | |
| <!-- Bulk IPL 2025 schedule hint --> | |
| <div class="card" style="max-width:700px; margin-top:1rem; padding:1rem; background:rgba(59,130,246,0.05); border-color:rgba(59,130,246,0.2);"> | |
| <div style="font-size:0.875rem; color:var(--muted2);"> | |
| <strong style="color:var(--blue);">💡 Tip:</strong> | |
| IPL matches typically kick off at <strong>7:30 PM IST</strong> with afternoon matches at <strong>3:30 PM IST</strong> on weekends. | |
| Add matches before the season starts so your team can predict early! | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ── TAB: RESULTS ─────────────────────────────────────────────────────── --> | |
| <div id="tab-results" class="tab-panel" style="display:none;"> | |
| {% set settable = matches|selectattr('status','in',['completed','live','locked','abandoned'])|list %} | |
| {% if settable %} | |
| <div style="display:grid; gap:1rem;"> | |
| {% for match in settable %} | |
| <div class="card"> | |
| <div style="display:flex; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; gap:1rem;"> | |
| <div> | |
| <div style="font-family:var(--font-display); font-size:1.3rem;"> | |
| <span style="color:{{ match.team1_color }};">{{ match.team1_abbr }}</span> | |
| <span style="color:var(--muted);"> vs </span> | |
| <span style="color:{{ match.team2_color }};">{{ match.team2_abbr }}</span> | |
| <span class="badge badge-{{ match.status }}" style="margin-left:0.5rem; vertical-align:middle;">{{ match.status }}</span> | |
| </div> | |
| <div style="font-size:0.85rem; color:var(--muted2);"> | |
| Match #{{ match.match_number or '?' }} · {{ match.match_date|format_date }} · {{ match.match_time }} | |
| {% if match.venue %} · {{ match.venue }}{% endif %} | |
| </div> | |
| {% if match.winner %} | |
| <div style="margin-top:0.5rem; font-size:0.85rem; color:var(--green);"> | |
| Current: 🏆 {{ match.winner }} {% if match.man_of_match %}· ⭐ {{ match.man_of_match }}{% endif %} | |
| {% if match.is_result_final %}<span class="badge badge-completed" style="margin-left:0.4rem;">Finalised</span>{% endif %} | |
| </div> | |
| {% endif %} | |
| </div> | |
| <form method="post" action="{{ url_for('admin_set_result', match_id=match.id) }}" class="admin-result-form" style="display:flex; flex-direction:column; gap:0.75rem; min-width:320px;"> | |
| <div class="form-row cols-2"> | |
| <div class="form-group" style="margin:0;"> | |
| <label>Winner *</label> | |
| <select name="winner" id="admin-winner-{{ match.id }}" required> | |
| <option value="">Select…</option> | |
| <option value="{{ match.team1 }}" {% if match.winner == match.team1 %}selected{% endif %}>{{ match.team1 }}</option> | |
| <option value="{{ match.team2 }}" {% if match.winner == match.team2 %}selected{% endif %}>{{ match.team2 }}</option> | |
| </select> | |
| </div> | |
| <div class="form-group" style="margin:0;"> | |
| <label>Man of the Match</label> | |
| <select name="man_of_match" id="admin-motm-{{ match.id }}" data-initial-motm="{{ (match.man_of_match or '')|e }}"> | |
| <option value="">— Pick winner first —</option> | |
| </select> | |
| <div class="form-hint" style="font-size:0.72rem; margin-top:0.25rem;">Squad for the selected winning team only.</div> | |
| </div> | |
| </div> | |
| <div class="form-group" style="margin:0;"> | |
| <label>Notes (optional)</label> | |
| <input type="text" name="result_notes" placeholder="e.g. Won by 4 wickets" value="{{ match.result_notes or '' }}"> | |
| </div> | |
| {% if match.is_result_final %} | |
| <label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer; color:var(--muted2); font-size:0.85rem;"> | |
| <input type="checkbox" name="recalculate" value="1" style="width:auto;"> | |
| Reverse previous settlement & recalculate | |
| </label> | |
| {% endif %} | |
| <button type="submit" class="btn btn-success btn-sm"> | |
| {% if match.is_result_final %}🔄 Update & Recalculate{% else %}✅ Set Result & Settle{% endif %} | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| {% endfor %} | |
| </div> | |
| {% else %} | |
| <div class="empty-state card"> | |
| <div class="icon">✅</div> | |
| <div>No matches need results yet. Complete or lock a match first.</div> | |
| </div> | |
| {% endif %} | |
| </div> | |
| <!-- ── TAB: USERS ───────────────────────────────────────────────────────── --> | |
| <div id="tab-users" class="tab-panel" style="display:none;"> | |
| <!-- Add User Form --> | |
| <div class="card" style="max-width:600px; margin-bottom:1.5rem;"> | |
| <div class="card-title">👤 ADD NEW PLAYER</div> | |
| <form method="post" action="{{ url_for('admin_add_user') }}"> | |
| <div class="form-row cols-2"> | |
| <div class="form-group"> | |
| <label>Username *</label> | |
| <input type="text" name="username" placeholder="e.g. rahul_ipl" required> | |
| </div> | |
| <div class="form-group"> | |
| <label>Display Name</label> | |
| <input type="text" name="display_name" placeholder="e.g. Rahul Sharma"> | |
| </div> | |
| </div> | |
| <div class="form-row cols-2"> | |
| <div class="form-group"> | |
| <label>Password</label> | |
| <input type="password" name="password" placeholder="Leave blank for name-picker sign-in"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Starting Points</label> | |
| <input type="number" name="initial_points" value="{{ points_config.initial }}" min="100"> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary">➕ Add Player</button> | |
| </form> | |
| </div> | |
| <!-- Users List --> | |
| <div class="card"> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Player</th><th>Username</th><th style="text-align:right;">Points</th> | |
| <th>Status</th><th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for u in users %} | |
| <tr> | |
| <td> | |
| <div style="font-weight:600;">{{ u.display_name or u.username }}</div> | |
| <div style="font-size:0.75rem; color:var(--muted);">Joined {{ u.created_at[:10]|format_date }}</div> | |
| </td> | |
| <td style="font-family:var(--font-mono); color:var(--muted2);">@{{ u.username }}</td> | |
| <td style="text-align:right; font-family:var(--font-mono); font-weight:700;">{{ '%.0f'|format(u.points) }}</td> | |
| <td> | |
| {% if u.is_active %}<span class="badge badge-completed">Active</span> | |
| {% else %}<span class="badge badge-abandoned">Inactive</span>{% endif %} | |
| </td> | |
| <td> | |
| <div style="display:flex; gap:0.4rem; flex-wrap:wrap; align-items:center;"> | |
| <!-- Adjust points --> | |
| <form method="post" action="{{ url_for('admin_adjust_points', user_id=u.id) }}" style="display:flex; gap:0.3rem;"> | |
| <input type="number" name="amount" placeholder="±pts" style="width:75px; font-size:0.8rem; padding:0.3rem 0.5rem;"> | |
| <input type="text" name="reason" placeholder="reason" style="width:100px; font-size:0.8rem; padding:0.3rem 0.5rem;"> | |
| <button type="submit" class="btn btn-secondary btn-sm" style="padding:0.3rem 0.5rem;">Adjust</button> | |
| </form> | |
| <!-- Toggle active --> | |
| <form method="post" action="{{ url_for('admin_toggle_user', user_id=u.id) }}"> | |
| <button type="submit" class="btn btn-ghost btn-sm">{{ '🚫 Disable' if u.is_active else '✅ Enable' }}</button> | |
| </form> | |
| </div> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const ACTIVE_TAB_KEY = 'admin_tab'; | |
| const ADMIN_MOTM_BY_MATCH = {{ squads_by_match_id | tojson }}; | |
| function adminFillMotmForWinner(matchId) { | |
| const w = document.getElementById('admin-winner-' + matchId); | |
| const s = document.getElementById('admin-motm-' + matchId); | |
| if (!w || !s) return; | |
| const key = String(matchId); | |
| const byMatch = ADMIN_MOTM_BY_MATCH[key] || ADMIN_MOTM_BY_MATCH[matchId]; | |
| const saved = (s.getAttribute('data-initial-motm') || '').trim(); | |
| const team = w.value; | |
| s.innerHTML = ''; | |
| const ph = document.createElement('option'); | |
| ph.value = ''; | |
| if (!team) { | |
| ph.textContent = '— Pick winner first —'; | |
| s.appendChild(ph); | |
| return; | |
| } | |
| const players = (byMatch && byMatch[team]) ? byMatch[team] : []; | |
| ph.textContent = players.length ? '— Optional: choose MOTM —' : '— No squad for this team —'; | |
| s.appendChild(ph); | |
| for (let i = 0; i < players.length; i++) { | |
| const o = document.createElement('option'); | |
| o.value = players[i]; | |
| o.textContent = players[i]; | |
| s.appendChild(o); | |
| } | |
| if (saved && players.indexOf(saved) !== -1) { | |
| s.value = saved; | |
| } | |
| } | |
| function showTab(name) { | |
| document.querySelectorAll('.tab-panel').forEach(p => p.style.display = 'none'); | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('btn-secondary')); | |
| document.querySelectorAll('.tab-btn').forEach(b => b.classList.add('btn-ghost')); | |
| const panel = document.getElementById('tab-' + name); | |
| if (panel) panel.style.display = 'block'; | |
| const btn = document.querySelector(`[data-tab="${name}"]`); | |
| if (btn) { btn.classList.remove('btn-ghost'); btn.classList.add('btn-secondary'); } | |
| localStorage.setItem(ACTIVE_TAB_KEY, name); | |
| } | |
| // Restore last tab or default | |
| const last = localStorage.getItem(ACTIVE_TAB_KEY) || 'matches'; | |
| showTab(last); | |
| document.querySelectorAll('[id^="admin-winner-"]').forEach(function (el) { | |
| const id = el.id.replace('admin-winner-', ''); | |
| el.addEventListener('change', function () { adminFillMotmForWinner(id); }); | |
| if (el.value) { | |
| adminFillMotmForWinner(id); | |
| } | |
| }); | |
| </script> | |
| {% endblock %} | |