'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { Folder, File, FolderPlus, Upload, Trash2, Edit2, Download, Share2, Eye, EyeOff, Lock, Unlock, Globe, Shield, AlertCircle, MoreVertical, ChevronRight, ChevronDown, Search, Grid, List, Copy, X } from 'lucide-react'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, } from '@/components/ui/context-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, } from '@/components/ui/dropdown-menu'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Badge } from '@/components/ui/badge'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList } from '@/components/ui/breadcrumb'; import { useToast } from '@/hooks/use-toast'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { cn } from '@/lib/utils'; import { useSession } from 'next-auth/react'; import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone"; import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list"; import { decryptWithServerAction } from '@/components/drm/drmUtils'; import { Progress } from '@/components/ui/progress'; // Import the secure viewer component import { SecurePDFViewer } from './SecurePDFViewer'; interface FileItem { id: string; name: string; type: 'file' | 'folder'; size?: number; mimeType?: string; category: 'public' | 'restricted' | 'confidential' | 'internal'; externalAccessLevel?: 'view_only' | 'view_download' | 'full_access'; updatedAt: Date; permissions?: { canView: boolean; canDownload: boolean; canEdit: boolean; canDelete: boolean; }; downloadCount?: number; viewCount?: number; parentId?: string | null; children?: FileItem[]; } interface UploadingFile { file: File; progress: number; status: 'pending' | 'uploading' | 'processing' | 'completed' | 'error'; error?: string; } interface FileManagerProps { projectId: string; } // Category configuration with icons and colors const categoryConfig = { public: { icon: Globe, color: 'text-green-500', label: 'Public' }, restricted: { icon: Eye, color: 'text-yellow-500', label: 'Restricted' }, confidential: { icon: Lock, color: 'text-red-500', label: 'Confidential' }, internal: { icon: Shield, color: 'text-blue-500', label: 'Internal' }, }; // Tree Item Component const TreeItem: React.FC<{ item: FileItem; level: number; expandedFolders: Set; selectedItems: Set; onToggleExpand: (id: string) => void; onSelectItem: (id: string) => void; onDoubleClick: (item: FileItem) => void; onView: (item: FileItem) => void; onDownload: (item: FileItem) => void; onDownloadFolder: (item: FileItem) => void; onDelete: (ids: string[]) => void; onShare: (item: FileItem) => void; onRename: (item: FileItem) => void; isInternalUser: boolean; }> = ({ item, level, expandedFolders, selectedItems, onToggleExpand, onSelectItem, onDoubleClick, onView, onDownload, onDownloadFolder, onDelete, onShare, onRename, isInternalUser }) => { const hasChildren = item.type === 'folder' && item.children && item.children.length > 0; const isExpanded = expandedFolders.has(item.id); const isSelected = selectedItems.has(item.id); const CategoryIcon = categoryConfig[item.category].icon; const categoryColor = categoryConfig[item.category].color; const categoryLabel = categoryConfig[item.category].label; const formatFileSize = (bytes?: number) => { if (!bytes) return '-'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; return ( <>
onSelectItem(item.id)} onDoubleClick={() => onDoubleClick(item)} >
{item.type === 'folder' && ( )} {item.type === 'file' && (
)}
{item.type === 'folder' ? ( ) : ( )} {item.name} {categoryLabel} {formatFileSize(item.size)} {new Date(item.updatedAt).toLocaleDateString()} {item.type === 'file' && ( <> onView(item)}> View {item.permissions?.canDownload && ( onDownload(item)}> Download )} )} {item.type === 'folder' && ( onDownloadFolder(item)}> Download Folder )} {isInternalUser && ( <> onShare(item)}> Share {item.permissions?.canEdit && ( onRename(item)}> Rename )} )} {item.permissions?.canDelete && ( <> onDelete([item.id])} > Delete )}
{item.type === 'folder' && isExpanded && item.children && (
{item.children.map((child) => ( ))}
)} ); }; export function FileManager({ projectId }: FileManagerProps) { const { data: session } = useSession(); const [items, setItems] = useState([]); const [treeItems, setTreeItems] = useState([]); const [currentPath, setCurrentPath] = useState([]); const [currentParentId, setCurrentParentId] = useState(null); const [selectedItems, setSelectedItems] = useState>(new Set()); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [viewMode, setViewMode] = useState<'grid' | 'list'>('list'); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(false); console.log(items,"items") // Upload states const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const [uploadingFiles, setUploadingFiles] = useState([]); const [uploadCategory, setUploadCategory] = useState('confidential'); // Dialog states const [folderDialogOpen, setFolderDialogOpen] = useState(false); const [shareDialogOpen, setShareDialogOpen] = useState(false); const [permissionDialogOpen, setPermissionDialogOpen] = useState(false); const [renameDialogOpen, setRenameDialogOpen] = useState(false); const [viewerDialogOpen, setViewerDialogOpen] = useState(false); const [viewerFileUrl, setViewerFileUrl] = useState(null); // Dialog data const [dialogValue, setDialogValue] = useState(''); const [selectedCategory, setSelectedCategory] = useState('confidential'); const [selectedFile, setSelectedFile] = useState(null); const [shareSettings, setShareSettings] = useState({ accessLevel: 'view_only', password: '', expiresAt: '', maxDownloads: '', }); const { toast } = useToast(); // Check if user is internal const isInternalUser = session?.user?.domain !== 'partners'; // Build tree structure function const buildTree = (flatItems: FileItem[]): FileItem[] => { const itemMap = new Map(); const rootItems: FileItem[] = []; // Store all items in map (initialize children) flatItems.forEach(item => { itemMap.set(item.id, { ...item, children: [] }); }); // Set parent-child relationships flatItems.forEach(item => { const mappedItem = itemMap.get(item.id)!; if (!item.parentId) { // No parentId means root item rootItems.push(mappedItem); } else { // Has parentId, add to parent's children const parent = itemMap.get(item.parentId); if (parent) { if (!parent.children) parent.children = []; parent.children.push(mappedItem); } else { // Can't find parent, treat as root rootItems.push(mappedItem); } } }); return rootItems; }; // Fetch file list const fetchItems = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams(); // For tree view, get entire list if (viewMode === 'list') { params.append('viewMode', 'tree'); // Keep current path info for tree view (used for highlighting, etc.) if (currentParentId) params.append('currentParentId', currentParentId); } else { // For grid view, only get current folder contents if (currentParentId) params.append('parentId', currentParentId); } const response = await fetch(`/api/data-room/${projectId}?${params}`); if (!response.ok) throw new Error('Failed to fetch files'); const data = await response.json(); setItems(data); // Build tree structure if (viewMode === 'list') { const tree = buildTree(data); setTreeItems(tree); } } catch (error) { toast({ title: 'Error', description: 'Failed to load files.', variant: 'destructive', }); } finally { setLoading(false); } }, [projectId, currentParentId, viewMode, toast]); useEffect(() => { fetchItems(); }, [fetchItems]); // Create folder const createFolder = async () => { try { const response = await fetch(`/api/data-room/${projectId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: dialogValue, type: 'folder', category: selectedCategory, parentId: currentParentId, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to create folder'); } await fetchItems(); setFolderDialogOpen(false); setDialogValue(''); toast({ title: 'Success', description: 'Folder created successfully.', }); } catch (error: any) { toast({ title: 'Error', description: error.message || 'Failed to create folder.', variant: 'destructive', }); } }; // Handle file upload const handleFileUpload = async (files: FileList | File[]) => { const fileArray = Array.from(files); // Initialize uploading file list const newUploadingFiles: UploadingFile[] = fileArray.map(file => ({ file, progress: 0, status: 'pending' as const })); setUploadingFiles(newUploadingFiles); // Process each file upload for (let i = 0; i < fileArray.length; i++) { const file = fileArray[i]; try { // Update status: uploading setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'uploading', progress: 20 } : f )); // DRM decryption setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'processing', progress: 40 } : f )); const decryptedData = await decryptWithServerAction(file); // Create FormData const formData = new FormData(); const blob = new Blob([decryptedData], { type: file.type }); formData.append('file', blob, file.name); formData.append('category', uploadCategory); formData.append('fileSize', file.size.toString()); // Pass file size if (currentParentId) { formData.append('parentId', currentParentId); } // Update upload progress setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, progress: 60 } : f )); // API call const response = await fetch(`/api/data-room/${projectId}/upload`, { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Upload failed'); } // Success setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'completed', progress: 100 } : f )); } catch (error: any) { // Failure setUploadingFiles(prev => prev.map((f, idx) => idx === i ? { ...f, status: 'error', error: error.message || 'Upload failed' } : f )); } } // Refresh list after all uploads complete await fetchItems(); // Show toast if any files succeeded const successCount = newUploadingFiles.filter(f => f.status === 'completed').length; if (successCount > 0) { toast({ title: 'Upload Complete', description: `${successCount} file(s) uploaded successfully.`, }); } }; // Download folder const downloadFolder = async (folder: FileItem) => { if (folder.type !== 'folder') return; try { toast({ title: 'Checking Permissions', description: 'Verifying download permissions for folder contents...', }); // Call folder download API const response = await fetch(`/api/data-room/${projectId}/download-folder/${folder.id}`, { method: 'GET', }); if (!response.ok) { const error = await response.json(); // If there are files without permission, provide details if (error.unauthorizedFiles) { toast({ title: 'Insufficient Permissions', description: `No permission for ${error.unauthorizedFiles.length} file(s): ${error.unauthorizedFiles.join(', ')}`, variant: 'destructive', }); return; } throw new Error(error.error || 'Folder download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // Include folder name in filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const fileName = `${folder.name}_${timestamp}.zip`; const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast({ title: 'Download Complete', description: `${folder.name} folder downloaded successfully.`, }); } catch (error: any) { toast({ title: 'Error', description: error.message || 'Failed to download folder.', variant: 'destructive', }); } }; // Share file const shareFile = async () => { if (!selectedFile) return; try { const response = await fetch(`/api/data-room/${projectId}/share`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileId: selectedFile.id, ...shareSettings, }), }); if (!response.ok) { throw new Error('Failed to create share link'); } const data = await response.json(); // Copy share link to clipboard await navigator.clipboard.writeText(data.shareUrl); toast({ title: 'Share Link Created', description: 'Link copied to clipboard.', }); setShareDialogOpen(false); setSelectedFile(null); } catch (error) { toast({ title: 'Error', description: 'Failed to create share link.', variant: 'destructive', }); } }; // Download multiple files const downloadMultipleFiles = async (itemIds: string[]) => { // Filter only actual files (exclude folders) that can be downloaded const filesToDownload = items.filter(item => itemIds.includes(item.id) && item.type === 'file' && item.permissions?.canDownload === 'true' ); if (filesToDownload.length === 0) { toast({ title: 'Notice', description: 'No downloadable files selected.', variant: 'default', }); return; } // Use regular download for single file if (filesToDownload.length === 1) { await downloadFile(filesToDownload[0]); return; } try { toast({ title: 'Preparing Download', description: `Compressing ${filesToDownload.length} files...`, }); // Call multiple files download API const response = await fetch(`/api/data-room/${projectId}/download-multiple`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileIds: filesToDownload.map(f => f.id) }) }); if (!response.ok) { throw new Error('Download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); // Include timestamp in filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const fileName = `files_${timestamp}.zip`; const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); toast({ title: 'Download Complete', description: `${filesToDownload.length} files downloaded successfully.`, }); } catch (error) { console.error('Multiple download error:', error); // Offer individual downloads on failure toast({ title: 'Batch Download Failed', description: 'Would you like to try individual downloads?', action: ( ), }); } }; // View file with PDFTron const viewFile = async (file: FileItem) => { try { setViewerFileUrl(file.filePath); setSelectedFile(file); setViewerDialogOpen(true); } catch (error) { toast({ title: 'Error', description: 'Failed to open file for viewing.', variant: 'destructive', }); } }; // Download file const downloadFile = async (file: FileItem) => { try { const response = await fetch(`/api/data-room/${projectId}/${file.id}/download`); if (!response.ok) { throw new Error('Download failed'); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = file.name; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { toast({ title: 'Error', description: 'Download failed.', variant: 'destructive', }); } }; // Delete files const deleteItems = async (itemIds: string[]) => { try { await Promise.all( itemIds.map(id => fetch(`/api/data-room/${projectId}/${id}`, { method: 'DELETE' }) ) ); await fetchItems(); setSelectedItems(new Set()); toast({ title: 'Success', description: 'Selected items deleted successfully.', }); } catch (error) { toast({ title: 'Error', description: 'Failed to delete items.', variant: 'destructive', }); } }; // Rename item const renameItem = async () => { if (!selectedFile) return; try { const response = await fetch( `/api/data-room/${projectId}/${selectedFile.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: dialogValue }), } ); if (!response.ok) { throw new Error('Failed to rename'); } await fetchItems(); setRenameDialogOpen(false); setSelectedFile(null); setDialogValue(''); toast({ title: 'Success', description: 'Item renamed successfully.', }); } catch (error) { toast({ title: 'Error', description: 'Failed to rename item.', variant: 'destructive', }); } }; // Change category const changeCategory = async ( itemId: string, newCategory: string, applyToChildren: boolean = false ) => { try { const response = await fetch( `/api/data-room/${projectId}/${itemId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ category: newCategory, applyToChildren }), } ); if (!response.ok) { throw new Error('Failed to change category'); } await fetchItems(); toast({ title: 'Success', description: 'Category updated successfully.', }); } catch (error) { toast({ title: 'Error', description: 'Failed to change category.', variant: 'destructive', }); } }; // Category change dialog states const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); const [applyToChildren, setApplyToChildren] = useState(false); const [newCategory, setNewCategory] = useState('confidential'); // Handle folder double click const handleFolderOpen = (folder: FileItem) => { if (viewMode === 'grid') { setCurrentPath([...currentPath, folder.name]); setCurrentParentId(folder.id); } else { // In tree view, expand/collapse const newExpanded = new Set(expandedFolders); if (newExpanded.has(folder.id)) { newExpanded.delete(folder.id); } else { newExpanded.add(folder.id); } setExpandedFolders(newExpanded); } setSelectedItems(new Set()); }; // Toggle folder expansion const toggleFolderExpand = (folderId: string) => { const newExpanded = new Set(expandedFolders); if (newExpanded.has(folderId)) { newExpanded.delete(folderId); } else { newExpanded.add(folderId); } setExpandedFolders(newExpanded); }; // Toggle item selection const toggleItemSelection = (itemId: string) => { const newSelected = new Set(selectedItems); if (newSelected.has(itemId)) { newSelected.delete(itemId); } else { newSelected.add(itemId); } setSelectedItems(newSelected); }; // Navigate to path const navigateToPath = (index: number) => { if (index === -1) { setCurrentPath([]); setCurrentParentId(null); } else { setCurrentPath(currentPath.slice(0, index + 1)); // Need to update parentId logic } }; // Filtered items const filteredItems = items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); const filteredTreeItems = treeItems.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()) ); // Format file size const formatFileSize = (bytes?: number) => { if (!bytes) return '-'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; }; return (
{/* Toolbar */}
{isInternalUser && ( <> )} {selectedItems.size > 0 && ( <> {/* Multiple download button */} {items.filter(item => selectedItems.has(item.id) && item.type === 'file' && item.permissions?.canDownload ==='true' ).length > 0 && ( )} {/* Delete button */} {items.find(item => selectedItems.has(item.id))?.permissions?.canDelete && ( )} )} {!isInternalUser && ( External User )}
setSearchQuery(e.target.value)} />
{/* Breadcrumb */} navigateToPath(-1)}> Home {currentPath.map((path, index) => ( navigateToPath(index)}> {path} ))}
{/* File List */} {loading ? (
Loading...
) : filteredItems.length === 0 ? (

Empty

) : viewMode === 'grid' ? (
{filteredItems.map((item) => { const CategoryIcon = categoryConfig[item.category].icon; const categoryColor = categoryConfig[item.category].color; return (
toggleItemSelection(item.id)} onDoubleClick={() => { if (item.type === 'folder') { handleFolderOpen(item); } }} >
{item.type === 'folder' ? ( ) : ( )}
{item.name} {item.viewCount !== undefined && (
{item.viewCount} {item.downloadCount !== undefined && ( {item.downloadCount} )}
)}
{item.type === 'folder' && ( <> handleFolderOpen(item)}> Open downloadFolder(item)}> Download Folder )} {item.type === 'file' && ( <> viewFile(item)}> View {item.permissions?.canDownload === 'true' && ( downloadFile(item)}> Download )} )} {isInternalUser && ( <> Change Category {Object.entries(categoryConfig).map(([key, config]) => ( { if (item.type === 'folder') { // Show dialog for folders setSelectedFile(item); setNewCategory(key); setCategoryDialogOpen(true); } else { // Change immediately for files changeCategory(item.id, key, false); } }} > {config.label} ))} { setSelectedFile(item); setShareDialogOpen(true); }} > Share {item.permissions?.canEdit && ( { setSelectedFile(item); setDialogValue(item.name); setRenameDialogOpen(true); }}> Rename )} )} {item.permissions?.canDelete && ( <> deleteItems([item.id])} > Delete )}
); })}
) : ( // Tree View
{filteredTreeItems.map((item) => ( { setSelectedFile(item); setShareDialogOpen(true); }} onRename={(item) => { setSelectedFile(item); setDialogValue(item.name); setRenameDialogOpen(true); }} isInternalUser={isInternalUser} /> ))}
)}
{/* Upload Dialog */} Upload Files Drag and drop files or click to select.
{/* Category Selection */}
{/* Dropzone */} { handleFileUpload(acceptedFiles); }} accept={{ 'application/pdf': ['.pdf'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], 'application/vnd.ms-excel': ['.xls'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], 'application/vnd.ms-powerpoint': ['.ppt'], 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], 'text/plain': ['.txt'], 'text/csv': ['.csv'], 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'], 'application/zip': ['.zip'], 'application/x-rar-compressed': ['.rar'], 'application/x-7z-compressed': ['.7z'], 'application/x-dwg': ['.dwg'], 'application/x-dxf': ['.dxf'], }} multiple={true} disabled={false} >
Drag files or click to upload Multiple files can be uploaded simultaneously
{/* Uploading File List */} {uploadingFiles.length > 0 && ( Uploading Files {uploadingFiles.map((uploadFile, index) => ( {uploadFile.file.name}
{uploadFile.file.size} {uploadFile.status === 'uploading' && Uploading...} {uploadFile.status === 'processing' && Processing...} {uploadFile.status === 'completed' && ( Complete )} {uploadFile.status === 'error' && ( {uploadFile.error} )}
{(uploadFile.status === 'uploading' || uploadFile.status === 'processing') && ( )}
{uploadFile.status === 'error' && ( )}
))}
)}
{/* Create Folder Dialog */} Create New Folder Set the folder name and access permission category.
setDialogValue(e.target.value)} placeholder="Enter folder name" />
{/* File Share Dialog */} Share File Sharing {selectedFile?.name}. Link Sharing Permission Settings
setShareSettings({ ...shareSettings, password: e.target.value })} placeholder="Enter password" />
setShareSettings({ ...shareSettings, expiresAt: e.target.value })} />
setShareSettings({ ...shareSettings, maxDownloads: e.target.value })} placeholder="Unlimited" />
{/* Rename Dialog */} Rename {selectedFile?.type === 'file' ? 'Enter the file name. (Extension will be preserved automatically)' : 'Enter the folder name.' }
setDialogValue(e.target.value)} placeholder={ selectedFile?.type === 'file' ? selectedFile.name.substring(0, selectedFile.name.lastIndexOf('.')) : selectedFile?.name } onKeyDown={(e) => { if (e.key === 'Enter') { renameItem(); } }} /> {selectedFile?.type === 'file' && (

Extension: {selectedFile.name.substring(selectedFile.name.lastIndexOf('.'))}

)}
{/* Category Change Dialog (for folders) */} Change Category Changing category for {selectedFile?.name} folder.
{Object.entries(categoryConfig).map(([key, config]) => (
setNewCategory(key)} >
{config.label}
{key === 'public' && 'External users can access freely'} {key === 'restricted' && 'External users can only view'} {key === 'confidential' && 'External users cannot access'} {key === 'internal' && 'Internal use only'}
))}
{selectedFile?.type === 'folder' && (
)}
{/* Secure Document Viewer Dialog */} { if (!open) { setViewerDialogOpen(false); setViewerFileUrl(null); setSelectedFile(null); } }} >
Secure Document Viewer
View Only Mode
Viewing: {selectedFile?.name} Protected Document - No Download/Copy/Print
{viewerFileUrl && selectedFile && ( { setViewerDialogOpen(false); setViewerFileUrl(null); setSelectedFile(null); }} /> )}
Viewer: {session?.user?.email} Time: {new Date().toLocaleString()} IP logged for security
); }