Download api/templates/shared/admin_bug_reports.html from mwask/anicove: direct link, hf CLI and curl.
- Browser
- Download file 24.6 kB
-
https://huggingface.co/spaces/mwask/anicove/resolve/main/api/templates/shared/admin_bug_reports.html
- Command line
-
hf download hf://spaces/mwask/anicove/api/templates/shared/admin_bug_reports.html
-
curl -L -o admin_bug_reports.html https://huggingface.co/spaces/mwask/anicove/resolve/main/api/templates/shared/admin_bug_reports.html
24.6 kB
| {% 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; | |
| } | |
| /* Stats bar */ | |
| .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; | |
| } | |
| /* Filters */ | |
| .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; | |
| } | |
| /* Broadcast bar */ | |
| .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; | |
| } | |
| /* Report list */ | |
| .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; | |
| } | |
| /* Replies */ | |
| .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; | |
| } | |
| /* Reply form */ | |
| .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; | |
| } | |
| /* Pagination */ | |
| .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; | |
| } | |
| /* Toast */ | |
| .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> | |
| <!-- Stats --> | |
| <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> | |
| <!-- Filters --> | |
| <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> | |
| <!-- Broadcast Bar --> | |
| <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> | |
| <!-- Reports --> | |
| <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> | |
| <!-- Pagination --> | |
| <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(''); | |
| // Pagination | |
| 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 %} | |