summaryrefslogtreecommitdiff
path: root/components/file-manager/FileManager copy.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-15 12:52:11 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-15 12:52:11 +0000
commitb54f6f03150dd78d86db62201b6386bf14b72394 (patch)
treeb3092bb34805fdc65eee5282e86a9fb90ba20d6e /components/file-manager/FileManager copy.tsx
parentc1bd1a2f499ee2f0742170021b37dab410983ab7 (diff)
(대표님) 커버, 데이터룸, 파일매니저, 담당자할당 등
Diffstat (limited to 'components/file-manager/FileManager copy.tsx')
-rw-r--r--components/file-manager/FileManager copy.tsx1852
1 files changed, 1852 insertions, 0 deletions
diff --git a/components/file-manager/FileManager copy.tsx b/components/file-manager/FileManager copy.tsx
new file mode 100644
index 00000000..3f3d73a4
--- /dev/null
+++ b/components/file-manager/FileManager copy.tsx
@@ -0,0 +1,1852 @@
+'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<string>;
+ selectedItems: Set<string>;
+ 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 (
+ <>
+ <div
+ className={cn(
+ "flex items-center p-2 rounded-lg cursor-pointer transition-colors",
+ "hover:bg-accent",
+ isSelected && "bg-accent"
+ )}
+ style={{ paddingLeft: `${level * 24 + 8}px` }}
+ onClick={() => onSelectItem(item.id)}
+ onDoubleClick={() => onDoubleClick(item)}
+ >
+ <div className="flex items-center mr-2">
+ {item.type === 'folder' && (
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ onToggleExpand(item.id);
+ }}
+ className="p-0.5 hover:bg-gray-200 rounded"
+ >
+ {isExpanded ? (
+ <ChevronDown className="h-4 w-4" />
+ ) : (
+ <ChevronRight className="h-4 w-4" />
+ )}
+ </button>
+ )}
+ {item.type === 'file' && (
+ <div className="w-5" />
+ )}
+ </div>
+
+ {item.type === 'folder' ? (
+ <Folder className="h-5 w-5 text-blue-500 mr-2" />
+ ) : (
+ <File className="h-5 w-5 text-gray-500 mr-2" />
+ )}
+
+ <span className="flex-1">{item.name}</span>
+
+ <Badge variant="outline" className="mr-2">
+ <CategoryIcon className={cn("h-3 w-3 mr-1", categoryColor)} />
+ {categoryLabel}
+ </Badge>
+
+ <span className="text-sm text-muted-foreground mr-4">
+ {formatFileSize(item.size)}
+ </span>
+ <span className="text-sm text-muted-foreground mr-2">
+ {new Date(item.updatedAt).toLocaleDateString()}
+ </span>
+
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="ghost" size="sm">
+ <MoreVertical className="h-4 w-4" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent>
+ {item.type === 'file' && (
+ <>
+ <DropdownMenuItem onClick={() => onView(item)}>
+ <Eye className="h-4 w-4 mr-2" />
+ View
+ </DropdownMenuItem>
+ {item.permissions?.canDownload && (
+ <DropdownMenuItem onClick={() => onDownload(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download
+ </DropdownMenuItem>
+ )}
+ </>
+ )}
+
+ {item.type === 'folder' && (
+ <DropdownMenuItem onClick={() => onDownloadFolder(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download Folder
+ </DropdownMenuItem>
+ )}
+
+ {isInternalUser && (
+ <>
+ <DropdownMenuItem onClick={() => onShare(item)}>
+ <Share2 className="h-4 w-4 mr-2" />
+ Share
+ </DropdownMenuItem>
+
+ {item.permissions?.canEdit && (
+ <DropdownMenuItem onClick={() => onRename(item)}>
+ <Edit2 className="h-4 w-4 mr-2" />
+ Rename
+ </DropdownMenuItem>
+ )}
+ </>
+ )}
+
+ {item.permissions?.canDelete && (
+ <>
+ <DropdownMenuSeparator />
+ <DropdownMenuItem
+ className="text-destructive"
+ onClick={() => onDelete([item.id])}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete
+ </DropdownMenuItem>
+ </>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </div>
+
+ {item.type === 'folder' && isExpanded && item.children && (
+ <div>
+ {item.children.map((child) => (
+ <TreeItem
+ key={child.id}
+ item={child}
+ level={level + 1}
+ expandedFolders={expandedFolders}
+ selectedItems={selectedItems}
+ onToggleExpand={onToggleExpand}
+ onSelectItem={onSelectItem}
+ onDoubleClick={onDoubleClick}
+ onView={onView}
+ onDownload={onDownload}
+ onDownloadFolder={onDownloadFolder}
+ onDelete={onDelete}
+ onShare={onShare}
+ onRename={onRename}
+ isInternalUser={isInternalUser}
+ />
+ ))}
+ </div>
+ )}
+ </>
+ );
+ };
+
+export function FileManager({ projectId }: FileManagerProps) {
+ const { data: session } = useSession();
+ const [items, setItems] = useState<FileItem[]>([]);
+ const [treeItems, setTreeItems] = useState<FileItem[]>([]);
+ const [currentPath, setCurrentPath] = useState<string[]>([]);
+ const [currentParentId, setCurrentParentId] = useState<string | null>(null);
+ const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
+ const [expandedFolders, setExpandedFolders] = useState<Set<string>>(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<UploadingFile[]>([]);
+ const [uploadCategory, setUploadCategory] = useState<string>('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<string | null>(null);
+
+ // Dialog data
+ const [dialogValue, setDialogValue] = useState('');
+ const [selectedCategory, setSelectedCategory] = useState<string>('confidential');
+ const [selectedFile, setSelectedFile] = useState<FileItem | null>(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<string, FileItem>();
+ 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: (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => {
+ // Execute individual downloads
+ filesToDownload.forEach(async (file, index) => {
+ // Add delay between downloads to reduce browser load
+ setTimeout(() => downloadFile(file), index * 500);
+ });
+ }}
+ >
+ Download Individually
+ </Button>
+ ),
+ });
+ }
+ };
+
+ // 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 (
+ <div className="flex flex-col h-full">
+ {/* Toolbar */}
+ <div className="border-b p-4">
+ <div className="flex items-center justify-between mb-3">
+ <div className="flex items-center gap-2">
+ {isInternalUser && (
+ <>
+ <Button
+ size="sm"
+ onClick={() => setFolderDialogOpen(true)}
+ >
+ <FolderPlus className="h-4 w-4 mr-1" />
+ New Folder
+ </Button>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => {
+ // 현재 폴더의 카테고리를 기본값으로 설정
+ if (currentParentId) {
+ const currentFolder = items.find(item => item.parentId === currentParentId);
+ if (currentFolder) {
+ setUploadCategory(currentFolder.category);
+ }
+ }
+ setUploadDialogOpen(true);
+ }}
+ >
+ <Upload className="h-4 w-4 mr-1" />
+ Upload
+ </Button>
+ </>
+ )}
+
+ {selectedItems.size > 0 && (
+ <>
+ {/* Multiple download button */}
+ {items.filter(item =>
+ selectedItems.has(item.id) &&
+ item.type === 'file' &&
+ item.permissions?.canDownload === 'true'
+ ).length > 0 && (
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => downloadMultipleFiles(Array.from(selectedItems))}
+ >
+ <Download className="h-4 w-4 mr-1" />
+ Download ({items.filter(item =>
+ selectedItems.has(item.id) && item.type === 'file'
+ ).length})
+ </Button>
+ )}
+
+ {/* Delete button */}
+ {items.find(item => selectedItems.has(item.id))?.permissions?.canDelete && (
+ <Button
+ size="sm"
+ variant="destructive"
+ onClick={() => deleteItems(Array.from(selectedItems))}
+ >
+ <Trash2 className="h-4 w-4 mr-1" />
+ Delete ({selectedItems.size})
+ </Button>
+ )}
+ </>
+ )}
+
+ {!isInternalUser && (
+ <Badge variant="secondary" className="ml-2">
+ <Shield className="h-3 w-3 mr-1" />
+ External User
+ </Badge>
+ )}
+ </div>
+
+ <div className="flex items-center gap-2">
+ <div className="relative">
+ <Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+ <Input
+ placeholder="Search..."
+ className="pl-8 w-64"
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ />
+ </div>
+
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
+ >
+ {viewMode === 'grid' ? <List className="h-4 w-4" /> : <Grid className="h-4 w-4" />}
+ </Button>
+ </div>
+ </div>
+
+ {/* Breadcrumb */}
+ <Breadcrumb>
+ <BreadcrumbList>
+ <BreadcrumbItem>
+ <BreadcrumbLink onClick={() => navigateToPath(-1)}>
+ Home
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ {currentPath.map((path, index) => (
+ <BreadcrumbItem key={index}>
+ <ChevronRight className="h-4 w-4" />
+ <BreadcrumbLink onClick={() => navigateToPath(index)}>
+ {path}
+ </BreadcrumbLink>
+ </BreadcrumbItem>
+ ))}
+ </BreadcrumbList>
+ </Breadcrumb>
+ </div>
+
+ {/* File List */}
+ <ScrollArea className="flex-1 p-4">
+ {loading ? (
+ <div className="flex justify-center items-center h-64">
+ <div className="text-muted-foreground">Loading...</div>
+ </div>
+ ) : filteredItems.length === 0 ? (
+ <div className="flex flex-col items-center justify-center h-64">
+ <Folder className="h-12 w-12 text-muted-foreground mb-2" />
+ <p className="text-muted-foreground">Empty</p>
+ </div>
+ ) : viewMode === 'grid' ? (
+ <div className="grid grid-cols-6 gap-4">
+ {filteredItems.map((item) => {
+ const CategoryIcon = categoryConfig[item.category].icon;
+ const categoryColor = categoryConfig[item.category].color;
+
+ return (
+ <ContextMenu key={item.id}>
+ <ContextMenuTrigger>
+ <div
+ className={cn(
+ "flex flex-col items-center p-3 rounded-lg cursor-pointer transition-colors",
+ "hover:bg-accent",
+ selectedItems.has(item.id) && "bg-accent"
+ )}
+ onClick={() => toggleItemSelection(item.id)}
+ onDoubleClick={() => {
+ if (item.type === 'folder') {
+ handleFolderOpen(item);
+ }
+ }}
+ >
+ <div className="relative">
+ {item.type === 'folder' ? (
+ <Folder className="h-12 w-12 text-blue-500" />
+ ) : (
+ <File className="h-12 w-12 text-gray-500" />
+ )}
+ <CategoryIcon className={cn("h-4 w-4 absolute -bottom-1 -right-1", categoryColor)} />
+ </div>
+
+ <span className="mt-2 text-sm text-center truncate w-full">
+ {item.name}
+ </span>
+
+ {item.viewCount !== undefined && (
+ <div className="flex items-center gap-2 mt-1">
+ <span className="text-xs text-muted-foreground flex items-center">
+ <Eye className="h-3 w-3 mr-1" />
+ {item.viewCount}
+ </span>
+ {item.downloadCount !== undefined && (
+ <span className="text-xs text-muted-foreground flex items-center">
+ <Download className="h-3 w-3 mr-1" />
+ {item.downloadCount}
+ </span>
+ )}
+ </div>
+ )}
+ </div>
+ </ContextMenuTrigger>
+
+ <ContextMenuContent>
+ {item.type === 'folder' && (
+ <>
+ <ContextMenuItem onClick={() => handleFolderOpen(item)}>
+ Open
+ </ContextMenuItem>
+ <ContextMenuItem onClick={() => downloadFolder(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download Folder
+ </ContextMenuItem>
+ </>
+ )}
+
+ {item.type === 'file' && (
+ <>
+ <ContextMenuItem onClick={() => viewFile(item)}>
+ <Eye className="h-4 w-4 mr-2" />
+ View
+ </ContextMenuItem>
+ {item.permissions?.canDownload === 'true' && (
+ <ContextMenuItem onClick={() => downloadFile(item)}>
+ <Download className="h-4 w-4 mr-2" />
+ Download
+ </ContextMenuItem>
+ )}
+ </>
+ )}
+
+ {isInternalUser && (
+ <>
+ <ContextMenuSeparator />
+ <ContextMenuSub>
+ <ContextMenuSubTrigger>
+ <Shield className="h-4 w-4 mr-2" />
+ Change Category
+ </ContextMenuSubTrigger>
+ <ContextMenuSubContent>
+ {Object.entries(categoryConfig).map(([key, config]) => (
+ <ContextMenuItem
+ key={key}
+ onClick={() => {
+ 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.icon className={cn("h-4 w-4 mr-2", config.color)} />
+ {config.label}
+ </ContextMenuItem>
+ ))}
+ </ContextMenuSubContent>
+ </ContextMenuSub>
+
+ <ContextMenuItem
+ onClick={() => {
+ setSelectedFile(item);
+ setShareDialogOpen(true);
+ }}
+ >
+ <Share2 className="h-4 w-4 mr-2" />
+ Share
+ </ContextMenuItem>
+
+ {item.permissions?.canEdit && (
+ <ContextMenuItem onClick={() => {
+ setSelectedFile(item);
+ setDialogValue(item.name);
+ setRenameDialogOpen(true);
+ }}>
+ <Edit2 className="h-4 w-4 mr-2" />
+ Rename
+ </ContextMenuItem>
+ )}
+ </>
+ )}
+
+ {item.permissions?.canDelete && (
+ <>
+ <ContextMenuSeparator />
+ <ContextMenuItem
+ className="text-destructive"
+ onClick={() => deleteItems([item.id])}
+ >
+ <Trash2 className="h-4 w-4 mr-2" />
+ Delete
+ </ContextMenuItem>
+ </>
+ )}
+ </ContextMenuContent>
+ </ContextMenu>
+ );
+ })}
+ </div>
+ ) : (
+ // Tree View
+ <div className="space-y-1">
+ {filteredTreeItems.map((item) => (
+ <TreeItem
+ key={item.id}
+ item={item}
+ level={0}
+ expandedFolders={expandedFolders}
+ selectedItems={selectedItems}
+ onToggleExpand={toggleFolderExpand}
+ onSelectItem={toggleItemSelection}
+ onDoubleClick={handleFolderOpen}
+ onView={viewFile}
+ onDownload={downloadFile}
+ onDownloadFolder={downloadFolder}
+ onDelete={deleteItems}
+ onShare={(item) => {
+ setSelectedFile(item);
+ setShareDialogOpen(true);
+ }}
+ onRename={(item) => {
+ setSelectedFile(item);
+ setDialogValue(item.name);
+ setRenameDialogOpen(true);
+ }}
+ isInternalUser={isInternalUser}
+ />
+ ))}
+ </div>
+ )}
+ </ScrollArea>
+
+{/* Upload Dialog */}
+<Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}>
+ <DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>Upload Files</DialogTitle>
+ <DialogDescription>
+ Drag and drop files or click to select.
+ </DialogDescription>
+ </DialogHeader>
+
+ <ScrollArea className="flex-1 pr-4">
+ <div className="space-y-4">
+ {/* Category Selection */}
+ <div>
+ <Label htmlFor="upload-category">Category</Label>
+ <Select value={uploadCategory} onValueChange={setUploadCategory}>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(categoryConfig)
+ .filter(([key]) => {
+ // 현재 폴더가 있는 경우
+ if (currentParentId) {
+ const currentFolder = items.find(item => item.parentId === currentParentId);
+ // 현재 폴더가 public이 아니면 public 옵션 제외
+ if (currentFolder && currentFolder.category !== 'public') {
+ return key !== 'public';
+ }
+ }
+ // 루트 폴더이거나 현재 폴더가 public인 경우 모든 옵션 표시
+ return true;
+ })
+ .map(([key, config]) => (
+ <SelectItem key={key} value={key}>
+ <div className="flex items-center">
+ <config.icon className={cn("h-4 w-4 mr-2", config.color)} />
+ <span>{config.label}</span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ {/* 현재 폴더 정보 표시 (선택사항) */}
+ {currentParentId && (() => {
+ const currentFolder = items.find(item => item.parentId === currentParentId);
+ if (currentFolder && currentFolder.category !== 'public') {
+ return (
+ <p className="text-xs text-muted-foreground mt-1 flex items-center">
+ <AlertCircle className="h-3 w-3 mr-1" />
+ Current folder is {categoryConfig[currentFolder.category].label}.
+ Public uploads are not allowed.
+ </p>
+ );
+ }
+ })()}
+ </div>
+
+ {/* Dropzone */}
+ <Dropzone
+ onDrop={(acceptedFiles: File[]) => {
+ 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}
+ >
+ <DropzoneZone className="h-48 border-2 border-dashed border-gray-300 rounded-lg">
+ <DropzoneInput />
+ <div className="flex flex-col items-center justify-center h-full">
+ <DropzoneUploadIcon className="h-12 w-12 text-muted-foreground mb-4" />
+ <DropzoneTitle>Drag files or click to upload</DropzoneTitle>
+ <DropzoneDescription>Multiple files can be uploaded simultaneously</DropzoneDescription>
+ </div>
+ </DropzoneZone>
+ </Dropzone>
+
+ {/* Uploading File List */}
+ {uploadingFiles.length > 0 && (
+ <div className="border rounded-lg p-4 bg-muted/50">
+ <div className="flex items-center justify-between mb-3">
+ <h4 className="font-medium text-sm">
+ Uploading Files ({uploadingFiles.filter(f => f.status === 'completed').length}/{uploadingFiles.length})
+ </h4>
+ {uploadingFiles.every(f => f.status === 'completed' || f.status === 'error') && (
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => setUploadingFiles([])}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ )}
+ </div>
+ <div className="space-y-2 max-h-[300px] overflow-y-auto">
+ {uploadingFiles.map((uploadFile, index) => (
+ <div key={index} className="flex items-start gap-3 p-3 bg-background rounded-md">
+ <File className="h-5 w-5 mt-0.5 flex-shrink-0 text-muted-foreground" />
+ <div className="flex-1 min-w-0">
+ <p className="text-sm font-medium truncate">{uploadFile.file.name}</p>
+ <div className="flex items-center gap-2 mt-1">
+ <span className="text-xs text-muted-foreground">
+ {formatFileSize(uploadFile.file.size)}
+ </span>
+ <span className="text-xs">
+ {uploadFile.status === 'pending' && 'Waiting...'}
+ {uploadFile.status === 'uploading' && 'Uploading...'}
+ {uploadFile.status === 'processing' && 'Processing...'}
+ {uploadFile.status === 'completed' && (
+ <span className="text-green-600 font-medium">✓ Complete</span>
+ )}
+ {uploadFile.status === 'error' && (
+ <span className="text-red-600 font-medium">✗ {uploadFile.error}</span>
+ )}
+ </span>
+ </div>
+ {(uploadFile.status === 'uploading' || uploadFile.status === 'processing') && (
+ <Progress value={uploadFile.progress} className="h-1.5 mt-2" />
+ )}
+ </div>
+ {uploadFile.status === 'error' && (
+ <Button
+ size="sm"
+ variant="ghost"
+ onClick={() => {
+ setUploadingFiles(prev =>
+ prev.filter((_, i) => i !== index)
+ );
+ }}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ )}
+ </div>
+ ))}
+ </div>
+ </div>
+ )}
+ </div>
+ </ScrollArea>
+
+ <DialogFooter className="mt-4">
+ <Button
+ variant="outline"
+ onClick={() => {
+ setUploadDialogOpen(false);
+ setUploadingFiles([]);
+ }}
+ >
+ Close
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+</Dialog>
+
+ {/* Create Folder Dialog */}
+ <Dialog open={folderDialogOpen} onOpenChange={setFolderDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Create New Folder</DialogTitle>
+ <DialogDescription>
+ Set the folder name and access permission category.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div>
+ <Label htmlFor="folder-name">Folder Name</Label>
+ <Input
+ id="folder-name"
+ value={dialogValue}
+ onChange={(e) => setDialogValue(e.target.value)}
+ placeholder="Enter folder name"
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="folder-category">Category</Label>
+ <Select value={selectedCategory} onValueChange={setSelectedCategory}>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {Object.entries(categoryConfig).map(([key, config]) => (
+ <SelectItem key={key} value={key}>
+ <div className="flex items-center">
+ <config.icon className={cn("h-4 w-4 mr-2", config.color)} />
+ <span>{config.label}</span>
+ </div>
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setFolderDialogOpen(false)}>
+ Cancel
+ </Button>
+ <Button onClick={createFolder}>Create</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* File Share Dialog */}
+ <Dialog open={shareDialogOpen} onOpenChange={setShareDialogOpen}>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <DialogTitle>Share File</DialogTitle>
+ <DialogDescription>
+ Sharing {selectedFile?.name}.
+ </DialogDescription>
+ </DialogHeader>
+
+ <Tabs defaultValue="link" className="w-full">
+ <TabsList className="grid w-full grid-cols-2">
+ <TabsTrigger value="link">Link Sharing</TabsTrigger>
+ <TabsTrigger value="permission">Permission Settings</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="link" className="space-y-4">
+ <div>
+ <Label htmlFor="access-level">Access Level</Label>
+ <Select
+ value={shareSettings.accessLevel}
+ onValueChange={(value) => setShareSettings({ ...shareSettings, accessLevel: value })}
+ >
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="view_only">
+ <div className="flex items-center">
+ <Eye className="h-4 w-4 mr-2" />
+ View Only
+ </div>
+ </SelectItem>
+ <SelectItem value="view_download">
+ <div className="flex items-center">
+ <Download className="h-4 w-4 mr-2" />
+ View + Download
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div>
+ <Label htmlFor="password">Password (Optional)</Label>
+ <Input
+ id="password"
+ type="password"
+ value={shareSettings.password}
+ onChange={(e) => setShareSettings({ ...shareSettings, password: e.target.value })}
+ placeholder="Enter password"
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="expires">Expiry Date (Optional)</Label>
+ <Input
+ id="expires"
+ type="datetime-local"
+ value={shareSettings.expiresAt}
+ onChange={(e) => setShareSettings({ ...shareSettings, expiresAt: e.target.value })}
+ />
+ </div>
+
+ <div>
+ <Label htmlFor="max-downloads">Max Downloads (Optional)</Label>
+ <Input
+ id="max-downloads"
+ type="number"
+ value={shareSettings.maxDownloads}
+ onChange={(e) => setShareSettings({ ...shareSettings, maxDownloads: e.target.value })}
+ placeholder="Unlimited"
+ />
+ </div>
+ </TabsContent>
+
+ <TabsContent value="permission" className="space-y-4">
+ <div>
+ <Label htmlFor="target-domain">Target Domain</Label>
+ <Select>
+ <SelectTrigger>
+ <SelectValue placeholder="Select domain" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="partners">Partners</SelectItem>
+ <SelectItem value="internal">Internal</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div className="space-y-2">
+ <Label>Permissions</Label>
+ <div className="space-y-2">
+ <div className="flex items-center justify-between">
+ <Label htmlFor="can-view" className="text-sm font-normal">View</Label>
+ <Switch id="can-view" defaultChecked />
+ </div>
+ <div className="flex items-center justify-between">
+ <Label htmlFor="can-download" className="text-sm font-normal">Download</Label>
+ <Switch id="can-download" />
+ </div>
+ <div className="flex items-center justify-between">
+ <Label htmlFor="can-edit" className="text-sm font-normal">Edit</Label>
+ <Switch id="can-edit" />
+ </div>
+ <div className="flex items-center justify-between">
+ <Label htmlFor="can-share" className="text-sm font-normal">Share</Label>
+ <Switch id="can-share" />
+ </div>
+ </div>
+ </div>
+ </TabsContent>
+ </Tabs>
+
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setShareDialogOpen(false)}>
+ Cancel
+ </Button>
+ <Button onClick={shareFile}>
+ <Share2 className="h-4 w-4 mr-2" />
+ Create Share Link
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* Rename Dialog */}
+ <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Rename</DialogTitle>
+ <DialogDescription>
+ {selectedFile?.type === 'file'
+ ? 'Enter the file name. (Extension will be preserved automatically)'
+ : 'Enter the folder name.'
+ }
+ </DialogDescription>
+ </DialogHeader>
+
+ <div>
+ <Label htmlFor="item-name">New Name</Label>
+ <Input
+ id="item-name"
+ value={dialogValue}
+ onChange={(e) => 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' && (
+ <p className="text-sm text-muted-foreground mt-1">
+ Extension: {selectedFile.name.substring(selectedFile.name.lastIndexOf('.'))}
+ </p>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setRenameDialogOpen(false);
+ setSelectedFile(null);
+ setDialogValue('');
+ }}
+ >
+ Cancel
+ </Button>
+ <Button onClick={renameItem}>Rename</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* Category Change Dialog (for folders) */}
+ <Dialog open={categoryDialogOpen} onOpenChange={setCategoryDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Change Category</DialogTitle>
+ <DialogDescription>
+ Changing category for {selectedFile?.name} folder.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div>
+ <Label>New Category</Label>
+ <div className="mt-2 space-y-2">
+ {Object.entries(categoryConfig).map(([key, config]) => (
+ <div
+ key={key}
+ className={cn(
+ "flex items-center p-3 rounded-lg border cursor-pointer transition-colors",
+ newCategory === key && "bg-accent border-primary"
+ )}
+ onClick={() => setNewCategory(key)}
+ >
+ <config.icon className={cn("h-5 w-5 mr-3", config.color)} />
+ <div className="flex-1">
+ <div className="font-medium">{config.label}</div>
+ <div className="text-sm text-muted-foreground">
+ {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'}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ {selectedFile?.type === 'folder' && (
+ <div className="flex items-center space-x-2">
+ <Switch
+ id="apply-to-children"
+ checked={newCategory !== 'public' ? true : applyToChildren}
+ onCheckedChange={(checked) => {
+ if (newCategory === 'public') {
+ setApplyToChildren(checked);
+ }
+ }}
+ disabled={newCategory !== 'public'}
+ />
+ <Label htmlFor="apply-to-children" className={cn(
+ newCategory !== 'public' && "text-muted-foreground"
+ )}>
+ Apply to all files and subfolders
+ {newCategory !== 'public' && (
+ <span className="text-xs block mt-1">
+ (Required for security categories)
+ </span>
+ )}
+ </Label>
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setCategoryDialogOpen(false);
+ setSelectedFile(null);
+ setApplyToChildren(false);
+ }}
+ >
+ Cancel
+ </Button>
+ <Button
+ onClick={() => {
+ if (selectedFile) {
+ changeCategory(selectedFile.id, newCategory, applyToChildren);
+ setCategoryDialogOpen(false);
+ setSelectedFile(null);
+ setApplyToChildren(false);
+ }
+ }}
+ >
+ Change
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* Secure Document Viewer Dialog */}
+ <Dialog
+ open={viewerDialogOpen}
+ onOpenChange={(open) => {
+ if (!open) {
+ setViewerDialogOpen(false);
+ setViewerFileUrl(null);
+ setSelectedFile(null);
+ }
+ }}
+ >
+ <DialogContent className="max-w-[90vw] max-h-[90vh] w-full h-full p-0">
+ <DialogHeader className="px-6 py-4 border-b">
+ <DialogTitle className="flex items-center justify-between">
+ <div className="flex items-center gap-2">
+ <Eye className="h-5 w-5" />
+ Secure Document Viewer
+ </div>
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Lock className="h-4 w-4" />
+ View Only Mode
+ </div>
+ </DialogTitle>
+ <DialogDescription>
+ <div className="flex items-center justify-between mt-2">
+ <span>Viewing: {selectedFile?.name}</span>
+ <Badge variant="destructive" className="text-xs">
+ <AlertCircle className="h-3 w-3 mr-1" />
+ Protected Document - No Download/Copy/Print
+ </Badge>
+ </div>
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="relative flex-1 h-[calc(90vh-120px)]">
+ {viewerFileUrl && selectedFile && (
+ <SecurePDFViewer
+ documentUrl={viewerFileUrl}
+ fileName={selectedFile.name}
+ onClose={() => {
+ setViewerDialogOpen(false);
+ setViewerFileUrl(null);
+ setSelectedFile(null);
+ }}
+ />
+ )}
+ </div>
+
+ <div className="px-6 py-3 border-t bg-muted/50">
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
+ <div className="flex items-center gap-4">
+ <span>Viewer: {session?.user?.email}</span>
+ <span>Time: {new Date().toLocaleString()}</span>
+ <span>IP logged for security</span>
+ </div>
+ <Button
+ size="sm"
+ variant="outline"
+ onClick={() => {
+ setViewerDialogOpen(false);
+ setViewerFileUrl(null);
+ setSelectedFile(null);
+ }}
+ >
+ <X className="h-4 w-4 mr-1" />
+ Close Viewer
+ </Button>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ </div>
+ );
+} \ No newline at end of file