Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | import { useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import type { AdminEventReportFilters } from '@/models/admin';
import { listAdminEventReports, updateAdminEventReportStatus } from '@/services/adminService';
import { useAdminListViewModel } from '@/viewmodels/admin/useAdminListViewModel';
import { useAdminMutation } from '@/viewmodels/admin/useAdminMutation';
import { BackofficeIdCell, BackofficeImageModal, BackofficePageShell, BackofficePagination, BackofficeStatusPill, BackofficeTableState, formatAdminDate } from './BackofficeListParts';
const INITIAL_FILTERS: AdminEventReportFilters = {
q: '',
status: 'PENDING',
report_category: undefined,
event_id: '',
reporter_user_id: '',
created_from: '',
created_to: '',
};
const REPORT_CATEGORY_LABELS: Record<NonNullable<AdminEventReportFilters['report_category']>, string> = {
SAFETY: 'Safety',
HARASSMENT: 'Harassment',
SPAM_OR_SCAM: 'Spam or scam',
INAPPROPRIATE_CONTENT: 'Inappropriate',
EVENT_NOT_AS_DESCRIBED: 'Not as described',
ILLEGAL_OR_DANGEROUS: 'Illegal or dangerous',
OTHER: 'Other',
};
export default function EventReportsAdminPage() {
const { token } = useAuth();
const initialFilters = useMemo(() => INITIAL_FILTERS, []);
const vm = useAdminListViewModel({ token, initialFilters, fetchPage: listAdminEventReports });
const mutation = useAdminMutation(vm.retry);
const [imageReportId, setImageReportId] = useState<string | null>(null);
const imageReport = vm.items.find((report) => report.id === imageReportId);
return (
<BackofficePageShell
title="Event Reports"
subtitle="Review user-submitted flags for inappropriate, harassing, or spam event content."
filters={(
<>
<input aria-label="Search reports" placeholder="Search event, reporter, or message" value={vm.filters.q ?? ''} onChange={(event) => vm.setFilter('q', event.target.value)} />
<select aria-label="Report status" value={vm.filters.status ?? ''} onChange={(event) => vm.setFilter('status', event.target.value === '' ? undefined : event.target.value as AdminEventReportFilters['status'])}>
<option value="">Any status</option>
<option value="PENDING">PENDING</option>
<option value="REVIEWED">REVIEWED</option>
<option value="DISMISSED">DISMISSED</option>
</select>
<select aria-label="Report category" value={vm.filters.report_category ?? ''} onChange={(event) => vm.setFilter('report_category', event.target.value === '' ? undefined : event.target.value as AdminEventReportFilters['report_category'])}>
<option value="">Any reason</option>
{Object.entries(REPORT_CATEGORY_LABELS).map(([value, label]) => (
<option key={value} value={value}>{label}</option>
))}
</select>
<input aria-label="Event ID" placeholder="Event ID" value={vm.filters.event_id ?? ''} onChange={(event) => vm.setFilter('event_id', event.target.value)} />
<input aria-label="Reporter user ID" placeholder="Reporter user ID" value={vm.filters.reporter_user_id ?? ''} onChange={(event) => vm.setFilter('reporter_user_id', event.target.value)} />
<input aria-label="Created from" type="datetime-local" value={vm.filters.created_from ?? ''} onChange={(event) => vm.setFilter('created_from', event.target.value)} />
<input aria-label="Created to" type="datetime-local" value={vm.filters.created_to ?? ''} onChange={(event) => vm.setFilter('created_to', event.target.value)} />
<button type="button" onClick={vm.applyFilters}>Apply</button>
<button type="button" className="bo-secondary-button" onClick={vm.clearFilters}>Clear</button>
</>
)}
>
<BackofficeTableState isLoading={vm.isLoading} error={vm.error} empty={vm.items.length === 0} onRetry={vm.retry} />
<div className="bo-table-wrap">
<table className="bo-table bo-report-table">
<thead>
<tr>
<th>ID</th>
<th>Status</th>
<th>Reason</th>
<th>Event</th>
<th>Reporter</th>
<th>Message</th>
<th>Image</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{vm.items.map((report) => (
<tr key={report.id}>
<td><BackofficeIdCell id={report.id} /></td>
<td>
<BackofficeStatusPill status={report.status} />
</td>
<td>{REPORT_CATEGORY_LABELS[report.report_category] ?? report.report_category}</td>
<td>
<div className="bo-report-linked-cell">
<Link to={`/events/${report.event_id}`}>{report.event_title ?? 'Open event'}</Link>
<span>{report.event_id}</span>
</div>
</td>
<td>
<div className="bo-report-linked-cell">
<strong>{report.reporter_username ?? 'Unknown user'}</strong>
<span>{report.reporter_email ?? report.reporter_user_id}</span>
</div>
</td>
<td className="bo-report-message">{report.message}</td>
<td>
{report.image_url ? (
<button type="button" className="bo-row-action" onClick={() => setImageReportId(report.id)}>View image</button>
) : (
'-'
)}
</td>
<td>{formatAdminDate(report.created_at)}</td>
<td>{formatAdminDate(report.updated_at)}</td>
<td>
<select
aria-label={`Change report status ${report.id}`}
value={report.status}
disabled={mutation.busyId === report.id}
onChange={(event) => void mutation.run(
report.id,
() => updateAdminEventReportStatus(token!, report.id, { status: event.target.value as typeof report.status }),
'Report status updated.',
)}
>
<option value="PENDING">PENDING</option>
<option value="REVIEWED">REVIEWED</option>
<option value="DISMISSED">DISMISSED</option>
</select>
</td>
</tr>
))}
</tbody>
</table>
</div>
{mutation.error && <div className="bo-state bo-state-error">{mutation.error}</div>}
{mutation.message && <div className="bo-result" role="status"><span>{mutation.message}</span></div>}
<BackofficePagination {...vm} onPrevious={vm.previousPage} onNext={vm.nextPage} onLimitChange={vm.changeLimit} />
<BackofficeImageModal
imageUrl={imageReport?.image_url ?? null}
title={imageReport?.event_title ?? 'Report image'}
message={imageReport?.message}
onClose={() => setImageReportId(null)}
/>
</BackofficePageShell>
);
}
|