/** * Component: Admin Reported Issues Section * Documentation: documentation/backend/services/reported-issues.md * * Displays open reported issues on the admin dashboard. * Allows dismiss or search-for-replacement actions. */ 'use client'; import React, { useState } from 'react'; import { createPortal } from 'react-dom'; import { useToast } from '@/components/ui/Toast'; import { formatDistanceToNow } from 'date-fns'; import { InteractiveTorrentSearchModal } from '@/components/requests/InteractiveTorrentSearchModal'; import { fetchJSON } from '@/lib/utils/api'; import { mutate } from 'swr'; interface ReportedIssue { id: string; reason: string; status: string; createdAt: string; audiobook: { id: string; title: string; author: string; coverArtUrl: string | null; audibleAsin: string | null; }; reporter: { id: string; plexUsername: string; avatarUrl: string | null; }; } interface ReportedIssuesSectionProps { issues: ReportedIssue[]; } export function ReportedIssuesSection({ issues }: ReportedIssuesSectionProps) { const toast = useToast(); const [loadingStates, setLoadingStates] = useState>({}); const [replaceIssue, setReplaceIssue] = useState(null); const handleDismiss = async (issueId: string) => { setLoadingStates((prev) => ({ ...prev, [issueId]: true })); try { await fetchJSON(`/api/admin/reported-issues/${issueId}/resolve`, { method: 'POST', body: JSON.stringify({ action: 'dismiss' }), }); toast.success('Issue dismissed'); await mutate((key: unknown) => typeof key === 'string' && key.includes('/api/admin/reported-issues')); } catch (error) { toast.error( `Failed to dismiss issue: ${error instanceof Error ? error.message : 'Unknown error'}` ); } finally { setLoadingStates((prev) => ({ ...prev, [issueId]: false })); } }; const handleReplaceSuccess = async () => { toast.success('Replacement download started'); setReplaceIssue(null); await mutate((key: unknown) => typeof key === 'string' && key.includes('/api/admin/reported-issues')); await mutate((key: unknown) => typeof key === 'string' && key.includes('/api/admin/metrics')); }; return ( <>
{/* Section Header */}

Reported Issues

{issues.length}
{/* Issues Grid */}
{issues.map((issue) => { const isLoading = loadingStates[issue.id] || false; return (
{/* Card Content */}
{/* Cover Image */}
{issue.audiobook.coverArtUrl ? ( {issue.audiobook.title} ) : (
)}
{/* Info */}

{issue.audiobook.title}

{issue.audiobook.author}

{/* Reporter */}
{issue.reporter.avatarUrl ? ( {issue.reporter.plexUsername} ) : (
)} {issue.reporter.plexUsername}
{/* Timestamp */}

{formatDistanceToNow(new Date(issue.createdAt), { addSuffix: true })}

{/* Reason */}

{issue.reason}

{/* Action Buttons */}
); })}
{/* Interactive Search Modal for Replacement */} {replaceIssue && createPortal(
setReplaceIssue(null)} onSuccess={handleReplaceSuccess} audiobook={{ title: replaceIssue.audiobook.title, author: replaceIssue.audiobook.author, }} asin={replaceIssue.audiobook.audibleAsin || undefined} replaceIssueId={replaceIssue.id} />
, document.body )} ); }