| {% extends "shared/base.html" %} |
|
|
| {% block title %}Bug Reports Admin - AniCove{% endblock %} |
| {% block meta_description %}Admin panel for managing bug reports.{% endblock %} |
|
|
| {% block extra_css %} |
| <style> |
| :root { |
| --abg-bg: #080808; |
| --abg-surface: #111; |
| --abg-border: #1e1e1e; |
| --abg-text: #e0e0e0; |
| --abg-muted: #777; |
| --abg-accent: #3b82f6; |
| --abg-success: #22c55e; |
| --abg-warning: #f59e0b; |
| --abg-error: #ef4444; |
| } |
| |
| .abg-page { |
| max-width: 1100px; |
| margin: 0 auto; |
| padding: 32px 20px; |
| } |
| |
| .abg-header { |
| margin-bottom: 28px; |
| } |
| |
| .abg-header h1 { |
| font-size: 1.5rem; |
| font-weight: 700; |
| margin: 0 0 4px 0; |
| color: #fff; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .abg-header p { |
| font-size: 0.85rem; |
| color: var(--abg-muted); |
| margin: 0; |
| } |
| |
| |
| .abg-stats { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); |
| gap: 10px; |
| margin-bottom: 24px; |
| } |
| |
| .abg-stat-card { |
| background: var(--abg-surface); |
| border: 1px solid var(--abg-border); |
| border-radius: 10px; |
| padding: 14px 16px; |
| text-align: center; |
| } |
| |
| .abg-stat-num { |
| font-size: 1.6rem; |
| font-weight: 700; |
| color: #fff; |
| line-height: 1.2; |
| } |
| |
| .abg-stat-label { |
| font-size: 0.7rem; |
| font-weight: 600; |
| text-transform: uppercase; |
| letter-spacing: 0.06em; |
| color: var(--abg-muted); |
| margin-top: 2px; |
| } |
| |
| |
| .abg-filters { |
| display: flex; |
| gap: 8px; |
| margin-bottom: 18px; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| |
| .abg-filter-btn { |
| padding: 6px 16px; |
| border-radius: 999px; |
| border: 1px solid var(--abg-border); |
| background: transparent; |
| color: #999; |
| font-size: 0.8rem; |
| font-weight: 500; |
| cursor: pointer; |
| transition: all 0.2s; |
| font-family: inherit; |
| } |
| |
| .abg-filter-btn:hover { |
| border-color: rgba(255,255,255,0.15); |
| color: #ddd; |
| } |
| |
| .abg-filter-btn.active { |
| border-color: var(--abg-accent); |
| background: rgba(59,130,246,0.12); |
| color: var(--abg-accent); |
| } |
| |
| .abg-filter-btn[data-status="open"].active { |
| border-color: #ef4444; |
| background: rgba(239,68,68,0.12); |
| color: #ef4444; |
| } |
| |
| .abg-filter-btn[data-status="resolved"].active { |
| border-color: #22c55e; |
| background: rgba(34,197,94,0.12); |
| color: #22c55e; |
| } |
| |
| |
| .abg-broadcast-bar { |
| display: none; |
| background: rgba(59,130,246,0.08); |
| border: 1px solid rgba(59,130,246,0.2); |
| border-radius: 10px; |
| padding: 12px 16px; |
| margin-bottom: 16px; |
| align-items: center; |
| gap: 12px; |
| flex-wrap: wrap; |
| } |
| |
| .abg-broadcast-bar.visible { |
| display: flex; |
| } |
| |
| .abg-broadcast-count { |
| font-size: 0.82rem; |
| color: var(--abg-accent); |
| font-weight: 600; |
| white-space: nowrap; |
| } |
| |
| .abg-broadcast-input { |
| flex: 1; |
| min-width: 200px; |
| padding: 8px 12px; |
| background: rgba(0,0,0,0.3); |
| border: 1px solid var(--abg-border); |
| border-radius: 8px; |
| color: #fff; |
| font-size: 0.85rem; |
| font-family: inherit; |
| } |
| |
| .abg-broadcast-input:focus { |
| outline: none; |
| border-color: var(--abg-accent); |
| } |
| |
| .abg-broadcast-btn { |
| padding: 8px 18px; |
| background: var(--abg-accent); |
| color: #fff; |
| border: none; |
| border-radius: 8px; |
| font-size: 0.82rem; |
| font-weight: 600; |
| cursor: pointer; |
| font-family: inherit; |
| transition: background 0.2s; |
| white-space: nowrap; |
| } |
| |
| .abg-broadcast-btn:hover { |
| background: #2563eb; |
| } |
| |
| .abg-broadcast-cancel { |
| padding: 8px 14px; |
| background: transparent; |
| color: #999; |
| border: 1px solid var(--abg-border); |
| border-radius: 8px; |
| font-size: 0.82rem; |
| cursor: pointer; |
| font-family: inherit; |
| transition: color 0.2s; |
| } |
| |
| .abg-broadcast-cancel:hover { |
| color: #fff; |
| } |
| |
| |
| .abg-report-list { |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| |
| .abg-report { |
| background: var(--abg-surface); |
| border: 1px solid var(--abg-border); |
| border-radius: 10px; |
| padding: 16px 18px; |
| transition: border-color 0.2s; |
| } |
| |
| .abg-report:hover { |
| border-color: rgba(255,255,255,0.08); |
| } |
| |
| .abg-report-header { |
| display: flex; |
| align-items: flex-start; |
| justify-content: space-between; |
| gap: 12px; |
| margin-bottom: 8px; |
| } |
| |
| .abg-report-title-area { |
| flex: 1; |
| min-width: 0; |
| } |
| |
| .abg-report-title { |
| font-size: 0.92rem; |
| font-weight: 600; |
| color: #ddd; |
| margin-bottom: 2px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| flex-wrap: wrap; |
| } |
| |
| .abg-report-category { |
| font-size: 0.65rem; |
| font-weight: 600; |
| padding: 1px 8px; |
| border-radius: 999px; |
| text-transform: uppercase; |
| letter-spacing: 0.05em; |
| } |
| |
| .abg-report-category.bug { |
| background: rgba(239,68,68,0.12); |
| color: #ef4444; |
| } |
| |
| .abg-report-category.feature { |
| background: rgba(34,197,94,0.12); |
| color: #22c55e; |
| } |
| |
| .abg-report-category.other { |
| background: rgba(245,158,11,0.12); |
| color: #f59e0b; |
| } |
| |
| .abg-report-author { |
| font-size: 0.75rem; |
| color: #555; |
| margin-top: 2px; |
| } |
| |
| .abg-report-actions { |
| display: flex; |
| gap: 6px; |
| flex-shrink: 0; |
| align-items: center; |
| } |
| |
| .abg-report-checkbox { |
| width: 18px; |
| height: 18px; |
| accent-color: var(--abg-accent); |
| cursor: pointer; |
| } |
| |
| .abg-report-body { |
| font-size: 0.83rem; |
| color: #999; |
| line-height: 1.6; |
| margin-bottom: 10px; |
| white-space: pre-wrap; |
| } |
| |
| .abg-report-meta { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| flex-wrap: wrap; |
| margin-bottom: 10px; |
| } |
| |
| .abg-status-select { |
| padding: 4px 10px; |
| border-radius: 6px; |
| border: 1px solid var(--abg-border); |
| background: rgba(0,0,0,0.3); |
| color: #ccc; |
| font-size: 0.78rem; |
| font-family: inherit; |
| cursor: pointer; |
| } |
| |
| .abg-status-select:focus { |
| outline: none; |
| border-color: var(--abg-accent); |
| } |
| |
| .abg-report-date { |
| font-size: 0.72rem; |
| color: #555; |
| } |
| |
| .abg-report-page-url { |
| font-size: 0.72rem; |
| color: #555; |
| background: rgba(255,255,255,0.03); |
| padding: 1px 6px; |
| border-radius: 4px; |
| } |
| |
| |
| .abg-replies { |
| margin-top: 10px; |
| padding-top: 10px; |
| border-top: 1px solid rgba(255,255,255,0.04); |
| } |
| |
| .abg-reply { |
| background: rgba(59,130,246,0.06); |
| border: 1px solid rgba(59,130,246,0.12); |
| border-radius: 8px; |
| padding: 10px 14px; |
| margin-bottom: 8px; |
| } |
| |
| .abg-reply:last-child { |
| margin-bottom: 0; |
| } |
| |
| .abg-reply-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 4px; |
| font-size: 0.72rem; |
| color: #555; |
| } |
| |
| .abg-reply-badge { |
| font-size: 0.65rem; |
| font-weight: 600; |
| padding: 1px 6px; |
| border-radius: 4px; |
| background: rgba(59,130,246,0.15); |
| color: var(--abg-accent); |
| text-transform: uppercase; |
| } |
| |
| .abg-reply-badge.broadcast { |
| background: rgba(245,158,11,0.15); |
| color: #f59e0b; |
| } |
| |
| .abg-reply-body { |
| font-size: 0.83rem; |
| color: #bbb; |
| line-height: 1.5; |
| } |
| |
| |
| .abg-reply-form { |
| display: flex; |
| gap: 8px; |
| margin-top: 10px; |
| align-items: flex-end; |
| } |
| |
| .abg-reply-input { |
| flex: 1; |
| padding: 8px 12px; |
| background: rgba(0,0,0,0.3); |
| border: 1px solid var(--abg-border); |
| border-radius: 8px; |
| color: #fff; |
| font-size: 0.83rem; |
| font-family: inherit; |
| resize: vertical; |
| min-height: 36px; |
| } |
| |
| .abg-reply-input:focus { |
| outline: none; |
| border-color: var(--abg-accent); |
| } |
| |
| .abg-reply-send { |
| padding: 8px 14px; |
| background: var(--abg-accent); |
| color: #fff; |
| border: none; |
| border-radius: 8px; |
| font-size: 0.82rem; |
| font-weight: 600; |
| cursor: pointer; |
| font-family: inherit; |
| white-space: nowrap; |
| transition: background 0.2s; |
| } |
| |
| .abg-reply-send:hover { |
| background: #2563eb; |
| } |
| |
| |
| .abg-pagination { |
| display: flex; |
| justify-content: center; |
| gap: 8px; |
| margin-top: 24px; |
| } |
| |
| .abg-page-btn { |
| padding: 6px 14px; |
| border-radius: 8px; |
| border: 1px solid var(--abg-border); |
| background: transparent; |
| color: #999; |
| font-size: 0.82rem; |
| cursor: pointer; |
| font-family: inherit; |
| transition: all 0.2s; |
| } |
| |
| .abg-page-btn:hover { |
| border-color: rgba(255,255,255,0.15); |
| color: #fff; |
| } |
| |
| .abg-page-btn:disabled { |
| opacity: 0.4; |
| cursor: not-allowed; |
| } |
| |
| |
| .abg-toast { |
| position: fixed; |
| bottom: 24px; |
| right: 24px; |
| background: var(--abg-surface); |
| border: 1px solid var(--abg-border); |
| border-radius: 10px; |
| padding: 12px 20px; |
| color: #bbb; |
| font-size: 0.85rem; |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); |
| z-index: 9999; |
| transform: translateY(20px); |
| opacity: 0; |
| transition: all 0.3s; |
| pointer-events: none; |
| } |
| |
| .abg-toast.visible { |
| transform: translateY(0); |
| opacity: 1; |
| } |
| |
| @media (max-width: 600px) { |
| .abg-page { padding: 20px 14px; } |
| .abg-report-header { flex-direction: column; } |
| .abg-report-actions { align-self: flex-end; } |
| } |
| </style> |
| {% endblock %} |
|
|
| {% block content %} <div class="abg-page"> |
| <a href="/admin" style="display:inline-flex;align-items:center;gap:5px;font-size:0.78rem;color:#555;text-decoration:none;margin-bottom:14px;transition:color 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='#555'"> |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg> |
| Back to Dashboard |
| </a> |
| <div class="abg-header"> |
| <h1> |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--abg-error);"> |
| <path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path> |
| <line x1="4" y1="22" x2="4" y2="15"></line> |
| </svg> |
| Bug Reports |
| <span style="font-size: 0.75rem; font-weight: 400; color: #555;">Admin Panel</span> |
| </h1> |
| <p>Manage, reply to, and track bug reports from users.</p> |
| </div> |
|
|
| |
| <div class="abg-stats" id="abg-stats"> |
| <div class="abg-stat-card"> |
| <div class="abg-stat-num" id="stat-total">-</div> |
| <div class="abg-stat-label">Total</div> |
| </div> |
| <div class="abg-stat-card"> |
| <div class="abg-stat-num" id="stat-open" style="color:#ef4444;">-</div> |
| <div class="abg-stat-label">Open</div> |
| </div> |
| <div class="abg-stat-card"> |
| <div class="abg-stat-num" id="stat-progress" style="color:#3b82f6;">-</div> |
| <div class="abg-stat-label">In Progress</div> |
| </div> |
| <div class="abg-stat-card"> |
| <div class="abg-stat-num" id="stat-resolved" style="color:#22c55e;">-</div> |
| <div class="abg-stat-label">Resolved</div> |
| </div> |
| </div> |
|
|
| |
| <div class="abg-filters"> |
| <button class="abg-filter-btn active" data-status="" onclick="filterReports(this, '')">All</button> |
| <button class="abg-filter-btn" data-status="open" onclick="filterReports(this, 'open')">🐛 Open</button> |
| <button class="abg-filter-btn" data-status="in_progress" onclick="filterReports(this, 'in_progress')">🔧 In Progress</button> |
| <button class="abg-filter-btn" data-status="resolved" onclick="filterReports(this, 'resolved')">✅ Resolved</button> |
| <button class="abg-filter-btn" data-status="closed" onclick="filterReports(this, 'closed')">📁 Closed</button> |
| </div> |
|
|
| |
| <div class="abg-broadcast-bar" id="abg-broadcast-bar"> |
| <span class="abg-broadcast-count" id="abg-broadcast-count">0 selected</span> |
| <textarea class="abg-broadcast-input" id="abg-broadcast-input" placeholder="Write a reply to send to all selected reports..." rows="1"></textarea> |
| <button class="abg-broadcast-btn" onclick="sendBroadcastReply()">Send to All</button> |
| <button class="abg-broadcast-cancel" onclick="clearSelection()">Cancel</button> |
| </div> |
|
|
| |
| <div class="abg-report-list" id="abg-report-list"> |
| <div style="text-align:center;padding:40px;color:#555;"> |
| <div class="bug-spinner" style="display:inline-block;width:24px;height:24px;border:2px solid rgba(255,255,255,0.1);border-top-color:#3b82f6;border-radius:50%;animation:bugSpin 0.6s linear infinite;margin-bottom:10px;"></div> |
| <div>Loading reports...</div> |
| </div> |
| </div> |
|
|
| |
| <div class="abg-pagination" id="abg-pagination" style="display:none;"></div> |
| </div> |
|
|
| <style> |
| @keyframes bugSpin { to { transform: rotate(360deg); } } |
| </style> |
| {% endblock %} |
|
|
| {% block extra_js %} |
| <script> |
| let currentFilter = ''; |
| let currentPage = 1; |
| let selectedReports = new Set(); |
| |
| function showToast(msg) { |
| let el = document.getElementById('abg-toast'); |
| if (!el) { |
| el = document.createElement('div'); |
| el.id = 'abg-toast'; |
| el.className = 'abg-toast'; |
| document.body.appendChild(el); |
| } |
| el.textContent = msg; |
| el.classList.add('visible'); |
| setTimeout(() => el.classList.remove('visible'), 3000); |
| } |
| |
| async function loadStats() { |
| try { |
| const res = await fetch('/api/bug-reports/admin/stats'); |
| const data = await res.json(); |
| if (data.success) { |
| document.getElementById('stat-total').textContent = data.stats.total; |
| document.getElementById('stat-open').textContent = data.stats.open; |
| document.getElementById('stat-progress').textContent = data.stats.in_progress; |
| document.getElementById('stat-resolved').textContent = data.stats.resolved; |
| } |
| } catch (e) {} |
| } |
| |
| function filterReports(btn, status) { |
| document.querySelectorAll('.abg-filter-btn').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| currentFilter = status; |
| currentPage = 1; |
| selectedReports.clear(); |
| updateBroadcastBar(); |
| loadReports(); |
| } |
| |
| function toggleSelectReport(id, checkbox) { |
| if (checkbox.checked) { |
| selectedReports.add(id); |
| } else { |
| selectedReports.delete(id); |
| } |
| updateBroadcastBar(); |
| } |
| |
| function updateBroadcastBar() { |
| const bar = document.getElementById('abg-broadcast-bar'); |
| const count = selectedReports.size; |
| document.getElementById('abg-broadcast-count').textContent = count + ' selected'; |
| if (count > 0) { |
| bar.classList.add('visible'); |
| } else { |
| bar.classList.remove('visible'); |
| } |
| } |
| |
| function clearSelection() { |
| selectedReports.clear(); |
| document.querySelectorAll('.abg-report-checkbox').forEach(cb => cb.checked = false); |
| document.getElementById('abg-broadcast-input').value = ''; |
| updateBroadcastBar(); |
| } |
| |
| async function sendBroadcastReply() { |
| const body = document.getElementById('abg-broadcast-input').value.trim(); |
| if (!body) { |
| showToast('Please write a reply message.'); |
| return; |
| } |
| if (selectedReports.size === 0) { |
| showToast('No reports selected.'); |
| return; |
| } |
| |
| const ids = Array.from(selectedReports); |
| try { |
| const res = await fetch('/api/bug-reports/broadcast-reply', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ report_ids: ids, body }), |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| showToast(`Reply sent to ${data.replied_to} report(s)!`); |
| clearSelection(); |
| loadReports(); |
| loadStats(); |
| } else { |
| showToast(data.message || 'Failed to send.'); |
| } |
| } catch (e) { |
| showToast('Network error.'); |
| } |
| } |
| |
| async function sendIndividualReply(reportId) { |
| const input = document.getElementById('reply-input-' + reportId); |
| const body = input.value.trim(); |
| if (!body) return; |
| |
| try { |
| const res = await fetch(`/api/bug-reports/${reportId}/reply`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ body }), |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| input.value = ''; |
| showToast('Reply sent!'); |
| loadReports(); |
| loadStats(); |
| } else { |
| showToast(data.message || 'Failed to send.'); |
| } |
| } catch (e) { |
| showToast('Network error.'); |
| } |
| } |
| |
| async function updateStatus(reportId, newStatus) { |
| try { |
| const res = await fetch(`/api/bug-reports/${reportId}/status`, { |
| method: 'PATCH', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ status: newStatus }), |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| showToast('Status updated!'); |
| loadReports(); |
| loadStats(); |
| } |
| } catch (e) {} |
| } |
| |
| async function loadReports() { |
| const listEl = document.getElementById('abg-report-list'); |
| listEl.innerHTML = '<div style="text-align:center;padding:40px;color:#555;"><div class="bug-spinner" style="display:inline-block;width:24px;height:24px;border:2px solid rgba(255,255,255,0.1);border-top-color:#3b82f6;border-radius:50%;animation:bugSpin 0.6s linear infinite;margin-bottom:10px;"></div><div>Loading...</div></div>'; |
| |
| try { |
| let url = `/api/bug-reports/admin/list?page=${currentPage}`; |
| if (currentFilter) url += `&status=${currentFilter}`; |
| const res = await fetch(url); |
| const data = await res.json(); |
| |
| if (!data.success) { |
| listEl.innerHTML = '<div style="text-align:center;padding:40px;color:var(--abg-error);">Failed to load reports.</div>'; |
| return; |
| } |
| |
| if (data.reports.length === 0) { |
| listEl.innerHTML = '<div style="text-align:center;padding:60px 20px;"><div style="font-size:2.5rem;margin-bottom:10px;opacity:0.5;">📭</div><div style="color:#555;">No reports found.</div></div>'; |
| document.getElementById('abg-pagination').style.display = 'none'; |
| return; |
| } |
| |
| listEl.innerHTML = data.reports.map(r => { |
| const statusLabels = { open: 'Open', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed' }; |
| const date = r.created_at ? new Date(r.created_at).toLocaleDateString() : ''; |
| const isSelected = selectedReports.has(r._id); |
| |
| let repliesHtml = ''; |
| const allMessages = []; |
| (r.admin_replies || []).forEach(rep => allMessages.push({ ...rep, type: 'admin' })); |
| (r.user_replies || []).forEach(rep => allMessages.push({ ...rep, type: 'user' })); |
| allMessages.sort((a, b) => (a.created_at || '').localeCompare(b.created_at || '')); |
| |
| if (allMessages.length > 0) { |
| repliesHtml = '<div class="abg-replies">' + allMessages.map(msg => { |
| const repDate = msg.created_at ? new Date(msg.created_at).toLocaleString() : ''; |
| if (msg.type === 'admin') { |
| return ` |
| <div class="abg-reply"> |
| <div class="abg-reply-header"> |
| <span>Admin ${repDate}</span> |
| ${msg.is_broadcast ? '<span class="abg-reply-badge broadcast">Broadcast</span>' : '<span class="abg-reply-badge">Direct</span>'} |
| </div> |
| <div class="abg-reply-body">${msg.body}</div> |
| </div> |
| `; |
| } else { |
| return ` |
| <div class="abg-reply" style="border-color: rgba(255,255,255,0.08); background: rgba(255,255,255,0.03);"> |
| <div class="abg-reply-header"> |
| <span>${r.author} (user) ${repDate}</span> |
| <span class="abg-reply-badge" style="background: rgba(255,255,255,0.08); color: #999;">User Reply</span> |
| </div> |
| <div class="abg-reply-body">${msg.body}</div> |
| </div> |
| `; |
| } |
| }).join('') + '</div>'; |
| } |
| |
| return ` |
| <div class="abg-report"> |
| <div class="abg-report-header"> |
| <div class="abg-report-title-area"> |
| <div class="abg-report-title"> |
| ${r.title} |
| <span class="abg-report-category ${r.category}">${r.category}</span> |
| </div> |
| <div class="abg-report-author">by ${r.author}</div> |
| </div> |
| <div class="abg-report-actions"> |
| <input type="checkbox" class="abg-report-checkbox" ${isSelected ? 'checked' : ''} onchange="toggleSelectReport('${r._id}', this)" title="Select for broadcast reply"> |
| </div> |
| </div> |
| <div class="abg-report-body">${r.body}</div> |
| <div class="abg-report-meta"> |
| <select class="abg-status-select" onchange="updateStatus('${r._id}', this.value)"> |
| <option value="open" ${r.status === 'open' ? 'selected' : ''}>🟢 Open</option> |
| <option value="in_progress" ${r.status === 'in_progress' ? 'selected' : ''}>🔧 In Progress</option> |
| <option value="resolved" ${r.status === 'resolved' ? 'selected' : ''}>✅ Resolved</option> |
| <option value="closed" ${r.status === 'closed' ? 'selected' : ''}>📁 Closed</option> |
| </select> |
| <span class="abg-report-date">${date}</span> |
| ${r.page_url ? `<span class="abg-report-page-url">📍 ${r.page_url}</span>` : ''} |
| </div> |
| ${repliesHtml} |
| <div class="abg-reply-form"> |
| <textarea class="abg-reply-input" id="reply-input-${r._id}" placeholder="Write a reply..." rows="1"></textarea> |
| <button class="abg-reply-send" onclick="sendIndividualReply('${r._id}')">Reply</button> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| |
| |
| const pagEl = document.getElementById('abg-pagination'); |
| const totalPages = Math.ceil(data.total / 30); |
| if (totalPages <= 1) { |
| pagEl.style.display = 'none'; |
| } else { |
| pagEl.style.display = 'flex'; |
| let pagHtml = ''; |
| pagHtml += `<button class="abg-page-btn" onclick="goToPage(${currentPage - 1})" ${currentPage <= 1 ? 'disabled' : ''}>Previous</button>`; |
| pagHtml += `<span style="color:#555;font-size:0.82rem;align-self:center;">Page ${currentPage} of ${totalPages}</span>`; |
| pagHtml += `<button class="abg-page-btn" onclick="goToPage(${currentPage + 1})" ${currentPage >= totalPages ? 'disabled' : ''}>Next</button>`; |
| pagEl.innerHTML = pagHtml; |
| } |
| |
| } catch (e) { |
| document.getElementById('abg-report-list').innerHTML = '<div style="text-align:center;padding:40px;color:var(--abg-error);">Error loading reports.</div>'; |
| } |
| } |
| |
| function goToPage(page) { |
| currentPage = page; |
| loadReports(); |
| } |
| |
| loadStats(); |
| loadReports(); |
| </script> |
| {% endblock %} |
|
|