feat: fix how complaints are displayed after resolution
Build Images and Deploy / Update-PROD-Stack (push) Successful in 19s

This commit is contained in:
2026-04-29 22:58:30 -04:00
parent db208ecdc4
commit 3af564983c
4 changed files with 84 additions and 10 deletions
+9
View File
@@ -585,6 +585,7 @@ const ComplaintReports = {
JOIN packages p ON c.package_id = p.id JOIN packages p ON c.package_id = p.id
JOIN hunts h ON c.hunt_id = h.id JOIN hunts h ON c.hunt_id = h.id
LEFT JOIN users u ON c.reported_by_user_id = u.id LEFT JOIN users u ON c.reported_by_user_id = u.id
WHERE c.status = 'open'
ORDER BY c.created_at ASC ORDER BY c.created_at ASC
`).all(); `).all();
}, },
@@ -607,6 +608,14 @@ const ComplaintReports = {
SET status = 'dismissed', reviewed_by = ?, reviewed_at = datetime('now'), resolution_note = ? SET status = 'dismissed', reviewed_by = ?, reviewed_at = datetime('now'), resolution_note = ?
WHERE id = ? WHERE id = ?
`).run(reviewedBy, resolutionNote || null, id); `).run(reviewedBy, resolutionNote || null, id);
},
updateModeration(id, reviewedBy, status, resolutionNote) {
db.prepare(`
UPDATE complaint_reports
SET status = ?, reviewed_by = ?, reviewed_at = datetime('now'), resolution_note = ?
WHERE id = ?
`).run(status, reviewedBy, resolutionNote || null, id);
} }
}; };
+27
View File
@@ -192,6 +192,33 @@ router.post('/hunts/:id/complaints/:complaintId/dismiss', requireHuntAccess, (re
res.redirect(`/admin/hunts/${hunt.id}`); res.redirect(`/admin/hunts/${hunt.id}`);
}); });
router.post('/hunts/:id/complaints/:complaintId/update', requireHuntAccess, (req, res) => {
const hunt = req.hunt;
const complaint = ComplaintReports.findById(parseInt(req.params.complaintId, 10));
if (!complaint || complaint.hunt_id !== hunt.id) {
req.session.flash = { type: 'danger', message: 'Complaint not found.' };
return res.redirect(`/admin/hunts/${hunt.id}`);
}
const statusMap = {
open: 'open',
resolved: 'resolved',
ignored: 'dismissed',
dismissed: 'dismissed'
};
const statusInput = String(req.body.status || '').toLowerCase();
const normalizedStatus = statusMap[statusInput];
if (!normalizedStatus) {
req.session.flash = { type: 'danger', message: 'Invalid complaint status.' };
return res.redirect(`/admin/hunts/${hunt.id}`);
}
const note = (req.body.note || '').trim();
ComplaintReports.updateModeration(complaint.id, req.session.userId, normalizedStatus, note || null);
req.session.flash = { type: 'success', message: 'Complaint updated.' };
res.redirect(`/admin/hunts/${hunt.id}`);
});
// ─── Manage user roles (admin only) ─────────────────────── // ─── Manage user roles (admin only) ───────────────────────
router.post('/users/:id/role', requireAdmin, (req, res) => { router.post('/users/:id/role', requireAdmin, (req, res) => {
const userId = parseInt(req.params.id, 10); const userId = parseInt(req.params.id, 10);
+11
View File
@@ -99,6 +99,9 @@
<div style="font-weight: 700;"> <div style="font-weight: 700;">
<a href="/admin/hunts/<%= c.hunt_id %>" style="color: var(--primary);"><%= c.hunt_name %></a> <a href="/admin/hunts/<%= c.hunt_id %>" style="color: var(--primary);"><%= c.hunt_name %></a>
&middot; Package #<%= c.card_number %> &middot; Package #<%= c.card_number %>
<% if (c.status !== 'open') { %>
<span class="badge" style="margin-left: 0.35rem; font-size: 0.68rem;"><%= c.status === 'dismissed' ? 'Ignored' : 'Resolved' %></span>
<% } %>
</div> </div>
<div style="font-size: 0.85rem; color: var(--muted);"> <div style="font-size: 0.85rem; color: var(--muted);">
<% if (c.reported_by_name) { %> <% if (c.reported_by_name) { %>
@@ -110,6 +113,14 @@
<% } %> <% } %>
&middot; <time datetime="<%= c.created_at %>"><%= new Date(c.created_at).toLocaleString() %></time> &middot; <time datetime="<%= c.created_at %>"><%= new Date(c.created_at).toLocaleString() %></time>
</div> </div>
<% if (c.status !== 'open') { %>
<div style="font-size: 0.85rem; color: var(--muted); margin-top: 0.2rem;">
Status: <strong><%= c.status === 'dismissed' ? 'Ignored' : 'Resolved' %></strong>
</div>
<% if (c.resolution_note) { %>
<p style="margin: 0.5rem 0 0; font-size: 0.9rem; padding: 0.65rem; background: var(--body-bg); border-radius: 6px;"><strong>Note:</strong> <%= c.resolution_note %></p>
<% } %>
<% } %>
</div> </div>
<a href="/admin/hunts/<%= c.hunt_id %>" class="btn btn-sm btn-outline">Review in Hunt</a> <a href="/admin/hunts/<%= c.hunt_id %>" class="btn btn-sm btn-outline">Review in Hunt</a>
</div> </div>
+31 -4
View File
@@ -98,13 +98,17 @@
<% } %> <% } %>
<% if (typeof complaints !== 'undefined' && complaints && complaints.length > 0) { %> <% if (typeof complaints !== 'undefined' && complaints && complaints.length > 0) { %>
<h2 style="margin-top: 1.5rem; margin-bottom: 1rem;">&#x1F6A9; Open Complaints <span class="badge" style="font-size: 0.75rem; vertical-align: middle;"><%= complaints.length %></span></h2> <h2 style="margin-top: 1.5rem; margin-bottom: 1rem;">&#x1F6A9; Complaints <span class="badge" style="font-size: 0.75rem; vertical-align: middle;"><%= complaints.length %></span></h2>
<div class="card" style="margin-bottom: 1rem;"> <div class="card" style="margin-bottom: 1rem;">
<% complaints.forEach(c => { %> <% complaints.forEach(c => { %>
<div style="padding: 1rem 0; border-bottom: 1px solid var(--border-color);"> <div style="padding: 1rem 0; border-bottom: 1px solid var(--border-color);">
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 0.75rem;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 0.75rem;">
<div style="flex: 1; min-width: 240px;"> <div style="flex: 1; min-width: 240px;">
<div style="font-weight: 700;">Package #<%= c.card_number %></div> <div style="font-weight: 700;">Package #<%= c.card_number %>
<% if (c.status !== 'open') { %>
<span class="badge" style="margin-left: 0.35rem; font-size: 0.68rem;"><%= c.status === 'dismissed' ? 'Ignored' : 'Resolved' %></span>
<% } %>
</div>
<div style="font-size: 0.85rem; color: var(--muted);"> <div style="font-size: 0.85rem; color: var(--muted);">
<% if (c.reported_by_name) { %> <% if (c.reported_by_name) { %>
Reporter: <a href="/player/<%= c.reported_by_username %>"><%= c.reported_by_name %></a> Reporter: <a href="/player/<%= c.reported_by_username %>"><%= c.reported_by_name %></a>
@@ -117,16 +121,39 @@
</div> </div>
<div style="font-size: 0.8rem; color: var(--muted); margin-top: 0.2rem;"><time datetime="<%= c.created_at %>"><%= new Date(c.created_at).toLocaleString() %></time></div> <div style="font-size: 0.8rem; color: var(--muted); margin-top: 0.2rem;"><time datetime="<%= c.created_at %>"><%= new Date(c.created_at).toLocaleString() %></time></div>
<p style="margin: 0.6rem 0 0; font-size: 0.95rem; background: var(--body-bg); border-radius: 6px; padding: 0.75rem;"><%= c.message %></p> <p style="margin: 0.6rem 0 0; font-size: 0.95rem; background: var(--body-bg); border-radius: 6px; padding: 0.75rem;"><%= c.message %></p>
<% if (c.status !== 'open') { %>
<div style="font-size: 0.85rem; color: var(--muted); margin-top: 0.45rem;">
Status: <strong><%= c.status === 'dismissed' ? 'Ignored' : 'Resolved' %></strong>
</div>
<% if (c.resolution_note) { %>
<p style="margin: 0.45rem 0 0; font-size: 0.9rem; background: var(--body-bg); border-radius: 6px; padding: 0.65rem;"><strong>Note:</strong> <%= c.resolution_note %></p>
<% } %>
<% } %>
</div> </div>
<div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
<% if (c.status === 'open') { %>
<form method="POST" action="/admin/hunts/<%= hunt.id %>/complaints/<%= c.id %>/resolve" style="margin:0; display:flex; gap:0.4rem; align-items:center;"> <form method="POST" action="/admin/hunts/<%= hunt.id %>/complaints/<%= c.id %>/resolve" style="margin:0; display:flex; gap:0.4rem; align-items:center;">
<input type="text" name="note" class="form-control" style="max-width: 220px;" placeholder="Optional resolution note"> <input type="text" name="note" class="form-control" style="max-width: 220px;" placeholder="Optional resolution note">
<button type="submit" class="btn btn-sm btn-success">Resolve</button> <button type="submit" class="btn btn-sm btn-success">Resolve</button>
</form> </form>
<form method="POST" action="/admin/hunts/<%= hunt.id %>/complaints/<%= c.id %>/dismiss" style="margin:0; display:flex; gap:0.4rem; align-items:center;"> <form method="POST" action="/admin/hunts/<%= hunt.id %>/complaints/<%= c.id %>/dismiss" style="margin:0; display:flex; gap:0.4rem; align-items:center;">
<input type="text" name="note" class="form-control" style="max-width: 220px;" placeholder="Optional dismissal note"> <input type="text" name="note" class="form-control" style="max-width: 220px;" placeholder="Optional ignore note">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Dismiss this complaint?')">Dismiss</button> <button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Ignore this complaint?')">Ignore</button>
</form> </form>
<% } else { %>
<details>
<summary class="btn btn-sm btn-outline" style="list-style: none; cursor: pointer;">Edit</summary>
<form method="POST" action="/admin/hunts/<%= hunt.id %>/complaints/<%= c.id %>/update" style="margin-top: 0.5rem; display:flex; gap:0.4rem; align-items:center; flex-wrap:wrap;">
<select name="status" class="form-control" style="max-width: 150px;">
<option value="resolved" <%= c.status === 'resolved' ? 'selected' : '' %>>Resolved</option>
<option value="ignored" <%= c.status === 'dismissed' ? 'selected' : '' %>>Ignored</option>
<option value="open">Open</option>
</select>
<input type="text" name="note" class="form-control" style="max-width: 260px;" placeholder="Optional note" value="<%= c.resolution_note || '' %>">
<button type="submit" class="btn btn-sm btn-primary">Save</button>
</form>
</details>
<% } %>
</div> </div>
</div> </div>
</div> </div>