diff --git a/src/app/(admin)/admin/projects/bulk-upload/page.tsx b/src/app/(admin)/admin/projects/bulk-upload/page.tsx index 0421210..b411e8d 100644 --- a/src/app/(admin)/admin/projects/bulk-upload/page.tsx +++ b/src/app/(admin)/admin/projects/bulk-upload/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useCallback, useRef } from 'react' +import { useState, useCallback, useRef, useEffect, useMemo } from 'react' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' @@ -46,6 +46,7 @@ import { Loader2, FileUp, AlertCircle, + ExternalLink, } from 'lucide-react' import { cn, formatFileSize } from '@/lib/utils' import { Pagination } from '@/components/shared/pagination' @@ -77,12 +78,13 @@ export default function BulkUploadPage() { label: string mimeTypes: string[] required: boolean - file: { id: string; fileName: string } | null + file: { id: string; fileName: string; bucket: string; objectKey: string } | null }> } | null>(null) const [bulkFiles, setBulkFiles] = useState>({}) const fileInputRefs = useRef>({}) + const utils = trpc.useUtils() // Debounce search const searchTimer = useRef>(undefined) @@ -109,6 +111,50 @@ export default function BulkUploadPage() { { enabled: !!roundId } ) + // Collect all files from current data for existence verification + const filesToVerify = useMemo(() => { + if (!data?.projects) return [] + const files: { bucket: string; objectKey: string }[] = [] + for (const row of data.projects) { + for (const req of row.requirements) { + if (req.file?.bucket && req.file?.objectKey) { + files.push({ bucket: req.file.bucket, objectKey: req.file.objectKey }) + } + } + } + return files + }, [data]) + + // Verify files actually exist in storage + const { data: fileExistence } = trpc.file.verifyFilesExist.useQuery( + { files: filesToVerify }, + { enabled: filesToVerify.length > 0, staleTime: 30_000 } + ) + + // Track which files are missing from storage (objectKey → true means missing) + const missingFiles = useMemo(() => { + if (!fileExistence) return new Set() + const missing = new Set() + for (const [key, exists] of Object.entries(fileExistence)) { + if (!exists) missing.add(key) + } + return missing + }, [fileExistence]) + + // Open file in new tab via presigned URL + const handleViewFile = useCallback( + async (bucket: string, objectKey: string) => { + try { + const { url } = await utils.file.getDownloadUrl.fetch({ bucket, objectKey }) + window.open(url, '_blank') + } catch { + toast.error('Failed to open file. It may have been deleted from storage.') + refetch() + } + }, + [utils, refetch] + ) + const uploadMutation = trpc.file.adminUploadForRoundRequirement.useMutation() // Upload a single file for a project requirement @@ -390,7 +436,7 @@ export default function BulkUploadPage() { {data.projects.map((row) => { const missingRequired = row.requirements.filter( - (r) => r.required && !r.file + (r) => r.required && (!r.file || (r.file?.objectKey && missingFiles.has(r.file.objectKey))) ) return ( + ) : req.file && req.file.objectKey && missingFiles.has(req.file.objectKey) ? ( +
+ + Missing + +
) : req.file || uploadState?.status === 'complete' ? (
- - {req.file?.fileName ?? 'Uploaded'} - + {req.file?.bucket && req.file?.objectKey ? ( + + ) : ( + + {req.file?.fileName ?? 'Uploaded'} + + )}
) : (