diff --git a/src/app/admin/components/RecentRequestsTable.tsx b/src/app/admin/components/RecentRequestsTable.tsx index 75b6916..5690a51 100644 --- a/src/app/admin/components/RecentRequestsTable.tsx +++ b/src/app/admin/components/RecentRequestsTable.tsx @@ -198,6 +198,8 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveB // View Details modal state const [viewDetailsAsin, setViewDetailsAsin] = useState(null); const [viewDetailsStatus, setViewDetailsStatus] = useState(null); + const [viewDetailsRequestId, setViewDetailsRequestId] = useState(null); + const [viewDetailsUserId, setViewDetailsUserId] = useState(null); // Build API URL with current local filters const apiUrl = `/api/admin/requests?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(debouncedSearch)}&status=${status}&userId=${userId}&sortBy=${sortBy}&sortOrder=${sortOrder}`; @@ -328,9 +330,16 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveB const hasActiveFilters = debouncedSearch || status !== 'all' || userId; // Action handlers - const handleViewDetails = (asin: string, requestStatus?: string) => { + const handleViewDetails = ( + asin: string, + requestStatus?: string, + requestId?: string, + userId?: string, + ) => { setViewDetailsAsin(asin); setViewDetailsStatus(requestStatus || null); + setViewDetailsRequestId(requestId || null); + setViewDetailsUserId(userId || null); }; const handleDeleteClick = (requestId: string, title: string) => { @@ -728,7 +737,7 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveB onManualSearch={handleManualSearch} onCancel={handleCancel} onRetryDownload={handleRetryDownload} - onViewDetails={(asin) => handleViewDetails(asin, request.status)} + onViewDetails={(asin) => handleViewDetails(asin, request.status, request.requestId, request.userId)} onFetchEbook={handleFetchEbook} onSearchTermsUpdated={() => mutate(apiUrl)} ebookSidecarEnabled={ebookSidecarEnabled} @@ -884,9 +893,13 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveB onClose={() => { setViewDetailsAsin(null); setViewDetailsStatus(null); + setViewDetailsRequestId(null); + setViewDetailsUserId(null); }} isAvailable={viewDetailsStatus === 'available' || viewDetailsStatus === 'completed'} requestStatus={viewDetailsStatus} + requestId={viewDetailsRequestId} + requestedByUserId={viewDetailsUserId} /> )} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index beb27c2..5e754e9 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -427,6 +427,8 @@ function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest onClose={() => { setDetailsAsin(null); setDetailsRequestId(null); }} requestStatus="awaiting_approval" requestedByUsername={detailsRequest?.user.plexUsername ?? null} + requestId={detailsRequestId} + requestedByUserId={detailsRequest?.user.id ?? null} adminActions={ diff --git a/src/components/audiobooks/AudiobookDetailsModal.tsx b/src/components/audiobooks/AudiobookDetailsModal.tsx index 2d790dc..17097d2 100644 --- a/src/components/audiobooks/AudiobookDetailsModal.tsx +++ b/src/components/audiobooks/AudiobookDetailsModal.tsx @@ -23,6 +23,7 @@ import { FolderArrowDownIcon, EyeSlashIcon } from '@heroicons/react/24/outline'; import { EyeSlashIcon as EyeSlashSolidIcon } from '@heroicons/react/24/solid'; import { fetchWithAuth } from '@/lib/utils/api'; import { useIsIgnored, useToggleIgnore } from '@/lib/hooks/useIgnoredAudiobooks'; +import { ADVANCEABLE_FROM_INTERACTIVE_SEARCH } from '@/lib/constants/request-statuses'; interface AudiobookDetailsModalProps { asin: string; @@ -35,6 +36,10 @@ interface AudiobookDetailsModalProps { requestStatus?: string | null; isAvailable?: boolean; requestedByUsername?: string | null; + /** In-flight request id for this audiobook (own or other user's). When set together with an advanceable status and own/admin viewer, Interactive Search → Download routes through select-torrent instead of creating a new request. */ + requestId?: string | null; + /** Owner of the in-flight request (matcher-populated). Used together with the viewer's id/role to gate advance-vs-create routing. */ + requestedByUserId?: string | null; hideRequestActions?: boolean; hasReportedIssue?: boolean; aiReason?: string | null; @@ -79,6 +84,8 @@ export function AudiobookDetailsModal({ requestStatus = null, isAvailable = false, requestedByUsername = null, + requestId = null, + requestedByUserId = null, hideRequestActions = false, hasReportedIssue = false, aiReason = null, @@ -89,7 +96,7 @@ export function AudiobookDetailsModal({ const { audiobook, audibleBaseUrl, isLoading, error } = useAudiobookDetails(isOpen ? asin : null); const { createRequest, isLoading: isRequesting } = useCreateRequest(); const { ebookStatus, revalidate: revalidateEbookStatus } = useEbookStatus(isOpen && isAvailable ? asin : null); - const { downloadAvailable, requestId } = useDownloadStatus(isOpen ? asin : null); + const { downloadAvailable, requestId: downloadRequestId } = useDownloadStatus(isOpen ? asin : null); const { fetchEbook, isLoading: isFetchingEbook } = useFetchEbookByAsin(); const { isIgnored, ignoredId, isLoading: isLoadingIgnore } = useIsIgnored(isOpen ? asin : null); @@ -118,6 +125,15 @@ export function AudiobookDetailsModal({ const status = getStatusInfo(isAvailable, effectiveStatus, requestedByUsername); const canShowEbookButtons = isAvailable && ebookStatus?.ebookSourcesEnabled && !ebookStatus?.hasActiveEbookRequest; + // Advance the existing request via select-torrent (instead of creating a new one) + // only when the viewer owns the request, or is admin, AND the status is one we route on. + // Outside this predicate the search modal falls back to today's "create new request" path. + const shouldAdvance = !!requestId + && !!user + && (requestedByUserId === user.id || user.role === 'admin') + && (ADVANCEABLE_FROM_INTERACTIVE_SEARCH as readonly string[]).includes(effectiveStatus ?? ''); + const advanceRequestId = shouldAdvance ? requestId ?? undefined : undefined; + useEffect(() => { setMounted(true); }, []); @@ -192,10 +208,10 @@ export function AudiobookDetailsModal({ }; const handleDownload = async () => { - if (!requestId) return; + if (!downloadRequestId) return; setIsDownloading(true); try { - const res = await fetchWithAuth(`/api/requests/${requestId}/download-token`, { method: 'POST' }); + const res = await fetchWithAuth(`/api/requests/${downloadRequestId}/download-token`, { method: 'POST' }); if (!res.ok) throw new Error('Failed to get download link'); const { downloadUrl } = await res.json(); window.location.href = downloadUrl; @@ -576,7 +592,7 @@ export function AudiobookDetailsModal({ )} {/* Download Link - subtle utility, visible from any context */} - {isAvailable && downloadAvailable && requestId && user?.permissions?.download !== false && ( + {isAvailable && downloadAvailable && downloadRequestId && user?.permissions?.download !== false && (

Download