diff --git a/documentation/TABLEOFCONTENTS.md b/documentation/TABLEOFCONTENTS.md index 39cc480..2b450b9 100644 --- a/documentation/TABLEOFCONTENTS.md +++ b/documentation/TABLEOFCONTENTS.md @@ -146,6 +146,7 @@ **"How do I delete requests?"** → [admin-features/request-deletion.md](admin-features/request-deletion.md) **"How do I approve/deny user requests?"** → [admin-features/request-approval.md](admin-features/request-approval.md) **"How do I enable auto-approve for requests?"** → [admin-features/request-approval.md](admin-features/request-approval.md) +**"How does the admin book info modal work?"** → [admin-features/request-approval.md](admin-features/request-approval.md#ui-features), [frontend/components.md](frontend/components.md#component-apis) **"How do I customize audiobook folder organization?"** → [settings-pages.md](settings-pages.md#audiobook-organization-template), [phase3/file-organization.md](phase3/file-organization.md#target-structure) **"How do I deploy?"** → [deployment/docker.md](deployment/docker.md) (multi-container), [deployment/unified.md](deployment/unified.md) (all-in-one) **"How do I use the unified container?"** → [deployment/unified.md](deployment/unified.md) diff --git a/documentation/admin-features/request-approval.md b/documentation/admin-features/request-approval.md index 8104090..c40af23 100644 --- a/documentation/admin-features/request-approval.md +++ b/documentation/admin-features/request-approval.md @@ -259,8 +259,11 @@ Update user (includes autoApproveRequests field) - Title and author - User avatar and username - Request timestamp (relative: "2 hours ago") + - Info button (ⓘ, top-right corner) — opens AudiobookDetailsModal for full book details - Approve button (green, checkmark icon) + - Search button (blue, magnifier icon) — opens InteractiveTorrentSearchModal - Deny button (red, X icon) +- **Info modal:** `AudiobookDetailsModal` rendered with `adminActions` prop containing Approve/Search/Deny buttons, allowing admin to review full book details (cover, description, series, genres, narrator, etc.) without leaving the approval workflow - Auto-refreshes every 10 seconds (SWR) - Loading states on buttons during approval/denial - Success/error toast notifications diff --git a/documentation/frontend/components.md b/documentation/frontend/components.md index fca5969..e1d6e3d 100644 --- a/documentation/frontend/components.md +++ b/documentation/frontend/components.md @@ -113,6 +113,7 @@ interface AudiobookDetailsModalProps { requestStatus?: string | null; isAvailable?: boolean; requestedByUsername?: string | null; + adminActions?: React.ReactNode; // Optional admin buttons (Approve/Search/Deny) rendered as second row in action bar } interface RequestCardProps { diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index a4837dc..de2b9cd 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -14,8 +14,10 @@ import { RecentRequestsTable } from './components/RecentRequestsTable'; import { ToastProvider, useToast } from '@/components/ui/Toast'; import { ReportedIssuesSection } from './components/ReportedIssuesSection'; import { InteractiveTorrentSearchModal } from '@/components/requests/InteractiveTorrentSearchModal'; +import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { BulkImportWizard } from '@/components/admin/BulkImportWizard'; import { TorrentResult } from '@/lib/utils/ranking-algorithm'; +import { InformationCircleIcon } from '@heroicons/react/24/outline'; import { formatDistanceToNow } from 'date-fns'; import { useState } from 'react'; @@ -60,11 +62,17 @@ function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest const toast = useToast(); const [loadingStates, setLoadingStates] = useState>({}); const [searchModalRequestId, setSearchModalRequestId] = useState(null); + const [detailsAsin, setDetailsAsin] = useState(null); + const [detailsRequestId, setDetailsRequestId] = useState(null); const searchModalRequest = searchModalRequestId ? requests.find((r) => r.id === searchModalRequestId) : null; + const detailsRequest = detailsRequestId + ? requests.find((r) => r.id === detailsRequestId) + : null; + const handleApproveRequest = async (requestId: string) => { setLoadingStates((prev) => ({ ...prev, [requestId]: true })); @@ -170,8 +178,22 @@ function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest return (
+ {/* Info Button — opens AudiobookDetailsModal */} + {request.audiobook.audibleAsin && ( + + )} + {/* Card Content */}
@@ -375,6 +397,66 @@ function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest }} /> )} + + {/* Book Details Modal — opened via info button on each approval card */} + {detailsAsin && detailsRequestId && ( + { setDetailsAsin(null); setDetailsRequestId(null); }} + requestStatus="awaiting_approval" + requestedByUsername={detailsRequest?.user.plexUsername ?? null} + adminActions={(() => { + const isLoading = loadingStates[detailsRequestId] || false; + return ( + <> + + + + + ); + })()} + /> + )}
); } diff --git a/src/components/audiobooks/AudiobookDetailsModal.tsx b/src/components/audiobooks/AudiobookDetailsModal.tsx index 9a7658c..7909146 100644 --- a/src/components/audiobooks/AudiobookDetailsModal.tsx +++ b/src/components/audiobooks/AudiobookDetailsModal.tsx @@ -38,6 +38,8 @@ interface AudiobookDetailsModalProps { hideRequestActions?: boolean; hasReportedIssue?: boolean; aiReason?: string | null; + /** Optional admin action buttons (Approve / Search / Deny) rendered as a second row in the action bar */ + adminActions?: React.ReactNode; } // Status helper @@ -80,6 +82,7 @@ export function AudiobookDetailsModal({ hideRequestActions = false, hasReportedIssue = false, aiReason = null, + adminActions, }: AudiobookDetailsModalProps) { const { user } = useAuth(); const { squareCovers } = usePreferences(); @@ -763,6 +766,13 @@ export function AudiobookDetailsModal({ )}
+ + {/* Admin Actions Row (Approve / Search / Deny) — injected by admin pages */} + {adminActions && ( +
+ {adminActions} +
+ )}
)}