erp-platform/ui/src/views/admin/files/FileManager.tsx
2025-10-26 11:59:02 +03:00

506 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback } from 'react'
import { Helmet } from 'react-helmet'
import { Button, Input, Select, toast, Notification, Spinner } from '@/components/ui'
import { FaFolder, FaCloudUploadAlt, FaSearch, FaTh, FaList, FaArrowUp } from 'react-icons/fa'
import Container from '@/components/shared/Container'
import { useLocalization } from '@/utils/hooks/useLocalization'
import fileManagementService from '@/services/fileManagement.service'
import Breadcrumb from './components/Breadcrumb'
import FileItem from './components/FileItem'
import FileUploadModal from './components/FileUploadModal'
import { CreateFolderModal, RenameItemModal, DeleteConfirmModal } from './components/FileModals'
import type {
FileItem as FileItemType,
BreadcrumbItem,
ViewMode,
SortBy,
SortOrder,
FileManagerFilters,
} from '@/types/fileManagement'
import classNames from 'classnames'
// Select options for sorting
const FileManager = () => {
const { translate } = useLocalization()
// State
const [loading, setLoading] = useState(true)
const [items, setItems] = useState<FileItemType[]>([])
const [filteredItems, setFilteredItems] = useState<FileItemType[]>([])
const [currentFolderId, setCurrentFolderId] = useState<string | undefined>()
const [breadcrumbItems, setBreadcrumbItems] = useState<BreadcrumbItem[]>([
{ name: 'Files', path: '', id: undefined },
])
const [selectedItems, setSelectedItems] = useState<string[]>([])
const [viewMode, setViewMode] = useState<ViewMode>('grid')
const [filters, setFilters] = useState<FileManagerFilters>({
searchTerm: '',
sortBy: 'name',
sortOrder: 'asc',
})
// Modal states
const [uploadModalOpen, setUploadModalOpen] = useState(false)
const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false)
const [renameModalOpen, setRenameModalOpen] = useState(false)
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
const [itemToRename, setItemToRename] = useState<FileItemType | undefined>()
const [itemsToDelete, setItemsToDelete] = useState<FileItemType[]>([])
// Loading states
const [uploading, setUploading] = useState(false)
const [creating, setCreating] = useState(false)
const [renaming, setRenaming] = useState(false)
const [deleting, setDeleting] = useState(false)
// Fetch items from API
const fetchItems = useCallback(async (folderId?: string) => {
try {
setLoading(true)
const response = await fileManagementService.getItems(folderId)
// Backend returns GetFilesDto which has Items property
setItems(response.data.items || [])
} catch (error) {
console.error('Failed to fetch items:', error)
toast.push(<Notification type="danger">Failed to load files and folders</Notification>)
} finally {
setLoading(false)
}
}, [])
// Fetch breadcrumb path
const fetchBreadcrumb = useCallback(async (folderId?: string) => {
try {
if (!folderId) {
setBreadcrumbItems([{ name: 'Files', path: '', id: undefined }])
return
}
const response = await fileManagementService.getFolderPath(folderId)
const pathItems: BreadcrumbItem[] = [
{ name: 'Files', path: '', id: undefined },
...response.data.path.map((item) => ({
name: item.name,
path: item.id,
id: item.id,
})),
]
setBreadcrumbItems(pathItems)
} catch (error) {
console.error('Failed to fetch breadcrumb:', error)
}
}, [])
// Initial load
useEffect(() => {
fetchItems(currentFolderId)
fetchBreadcrumb(currentFolderId)
}, [currentFolderId, fetchItems, fetchBreadcrumb])
// Filter and sort items
useEffect(() => {
let filtered = [...items]
// Apply search filter
if (filters.searchTerm) {
filtered = filtered.filter((item) =>
item.name.toLowerCase().includes(filters.searchTerm!.toLowerCase()),
)
}
// Apply sorting
filtered.sort((a, b) => {
let comparison = 0
switch (filters.sortBy) {
case 'name':
comparison = a.name.localeCompare(b.name)
break
case 'size':
comparison = (a.size || 0) - (b.size || 0)
break
case 'type':
comparison = a.type.localeCompare(b.type)
break
case 'modified':
comparison = new Date(a.modifiedAt).getTime() - new Date(b.modifiedAt).getTime()
break
}
return filters.sortOrder === 'desc' ? -comparison : comparison
})
// Folders first
filtered.sort((a, b) => {
if (a.type === 'folder' && b.type === 'file') return -1
if (a.type === 'file' && b.type === 'folder') return 1
return 0
})
setFilteredItems(filtered)
}, [items, filters])
// Navigation handlers
const handleBreadcrumbNavigate = (breadcrumb: BreadcrumbItem) => {
setCurrentFolderId(breadcrumb.id)
setSelectedItems([])
}
const handleItemSelect = (item: FileItemType) => {
setSelectedItems((prev) => {
if (prev.includes(item.id)) {
return prev.filter((id) => id !== item.id)
} else {
return [...prev, item.id]
}
})
}
const handleItemDoubleClick = (item: FileItemType) => {
if (item.type === 'folder') {
setCurrentFolderId(item.id)
setSelectedItems([])
}
}
// File operations
const handleUploadFiles = async (files: File[]) => {
try {
setUploading(true)
for (const file of files) {
await fileManagementService.uploadFile({
file,
parentId: currentFolderId,
})
}
await fetchItems(currentFolderId)
toast.push(<Notification type="success">Files uploaded successfully</Notification>)
} catch (error) {
console.error('Upload failed:', error)
toast.push(<Notification type="danger">Failed to upload files</Notification>)
throw error
} finally {
setUploading(false)
}
}
const handleCreateFolder = async (name: string) => {
try {
setCreating(true)
await fileManagementService.createFolder({
name,
parentId: currentFolderId,
})
await fetchItems(currentFolderId)
toast.push(<Notification type="success">Folder created successfully</Notification>)
} catch (error) {
console.error('Create folder failed:', error)
toast.push(<Notification type="danger">Failed to create folder</Notification>)
throw error
} finally {
setCreating(false)
}
}
const handleRenameItem = async (newName: string) => {
if (!itemToRename) return
try {
setRenaming(true)
await fileManagementService.renameItem({
id: itemToRename.id,
newName,
})
await fetchItems(currentFolderId)
toast.push(<Notification type="success">Item renamed successfully</Notification>)
} catch (error) {
console.error('Rename failed:', error)
toast.push(<Notification type="danger">Failed to rename item</Notification>)
throw error
} finally {
setRenaming(false)
}
}
const handleDeleteItems = async () => {
try {
setDeleting(true)
for (const item of itemsToDelete) {
await fileManagementService.deleteItem({ id: item.id })
}
await fetchItems(currentFolderId)
setSelectedItems([])
toast.push(<Notification type="success">Items deleted successfully</Notification>)
} catch (error) {
console.error('Delete failed:', error)
toast.push(<Notification type="danger">Failed to delete items</Notification>)
throw error
} finally {
setDeleting(false)
}
}
const handleDownload = async (item: FileItemType) => {
try {
const blob = await fileManagementService.downloadFile(item.id)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = item.name
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (error) {
console.error('Download failed:', error)
toast.push(<Notification type="danger">Failed to download file</Notification>)
}
}
// Action handlers
const openRenameModal = (item: FileItemType) => {
setItemToRename(item)
setRenameModalOpen(true)
}
const openDeleteModal = (items: FileItemType[]) => {
setItemsToDelete(items)
setDeleteModalOpen(true)
}
const goUpOneLevel = () => {
if (breadcrumbItems.length > 1) {
const parentBreadcrumb = breadcrumbItems[breadcrumbItems.length - 2]
handleBreadcrumbNavigate(parentBreadcrumb)
}
}
return (
<Container>
<Helmet
titleTemplate="%s | Sözsoft Kurs Platform"
title={translate('::' + 'App.Files')}
defaultTitle="Sözsoft Kurs Platform"
></Helmet>
{/* Toolbar */}
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4 mb-4 mt-2">
<div className="flex items-center gap-2">
<Button
variant="solid"
icon={<FaCloudUploadAlt />}
onClick={() => setUploadModalOpen(true)}
>
Upload Files
</Button>
<Button
variant="default"
icon={<FaFolder />}
onClick={() => setCreateFolderModalOpen(true)}
>
Create Folder
</Button>
{breadcrumbItems.length > 1 && (
<Button variant="default" icon={<FaArrowUp />} onClick={goUpOneLevel}>
Go Up
</Button>
)}
</div>
<div className="flex items-center gap-4">
{/* Search */}
<div className="flex items-center">
<Input
size="sm"
placeholder="Search files..."
value={filters.searchTerm}
onChange={(e) => setFilters((prev) => ({ ...prev, searchTerm: e.target.value }))}
prefix={<FaSearch className="text-gray-400" />}
className="w-64"
/>
</div>
{/* Sort */}
<Select
size="sm"
options={[
{ value: 'name-asc', label: 'Name (A-Z)' },
{ value: 'name-desc', label: 'Name (Z-A)' },
{ value: 'size-asc', label: 'Size (Small to Large)' },
{ value: 'size-desc', label: 'Size (Large to Small)' },
{ value: 'modified-desc', label: 'Modified (Newest)' },
{ value: 'modified-asc', label: 'Modified (Oldest)' },
]}
value={{
value: `${filters.sortBy}-${filters.sortOrder}`,
label: (() => {
const sortOptions = {
'name-asc': 'Name (A-Z)',
'name-desc': 'Name (Z-A)',
'size-asc': 'Size (Small to Large)',
'size-desc': 'Size (Large to Small)',
'modified-desc': 'Modified (Newest)',
'modified-asc': 'Modified (Oldest)',
}
return sortOptions[
`${filters.sortBy}-${filters.sortOrder}` as keyof typeof sortOptions
]
})(),
}}
onChange={(option) => {
if (option && 'value' in option) {
const [sortBy, sortOrder] = (option.value as string).split('-') as [
SortBy,
SortOrder,
]
setFilters((prev) => ({ ...prev, sortBy, sortOrder }))
}
}}
/>
{/* View Mode */}
<div className="flex border border-gray-300 dark:border-gray-600 rounded">
<Button
variant="plain"
size="sm"
icon={<FaTh />}
className={classNames(
'rounded-r-none border-r',
viewMode === 'grid' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('grid')}
/>
<Button
variant="plain"
size="sm"
icon={<FaList />}
className={classNames(
'rounded-l-none',
viewMode === 'list' && 'bg-blue-50 dark:bg-blue-900/20 text-blue-600',
)}
onClick={() => setViewMode('list')}
/>
</div>
</div>
</div>
{/* Breadcrumb */}
<div className="mb-6">
<Breadcrumb items={breadcrumbItems} onNavigate={handleBreadcrumbNavigate} />
</div>
{/* Files Grid/List */}
{loading ? (
<div className="flex justify-center items-center py-20">
<Spinner size="lg" />
</div>
) : (
<>
{/* List View Header */}
{viewMode === 'list' && (
<div className="grid grid-cols-12 gap-4 px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 border-b dark:border-gray-700 mb-2">
<div className="col-span-1"></div> {/* Icon column */}
<div className="col-span-4">İsim</div>
<div className="col-span-2">Tür</div>
<div className="col-span-2">Boyut</div>
<div className="col-span-2">Değiştirilme</div>
<div className="col-span-1"></div> {/* Actions column */}
</div>
)}
<div
className={classNames(
viewMode === 'grid'
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4'
: 'space-y-1',
)}
>
{filteredItems.length === 0 ? (
<div className="col-span-full text-center py-20">
<FaFolder className="mx-auto h-16 w-16 text-gray-400 mb-4" />
<p className="text-gray-500 dark:text-gray-400">
{filters.searchTerm ? 'No files match your search' : 'This folder is empty'}
</p>
</div>
) : (
filteredItems.map((item) => (
<FileItem
key={item.id}
item={item}
viewMode={viewMode}
selected={selectedItems.includes(item.id)}
onSelect={handleItemSelect}
onDoubleClick={handleItemDoubleClick}
onCreateFolder={(parentItem) => {
// Klasör içinde yeni klasör oluşturmak için parent klasörü set et
setCurrentFolderId(parentItem.id)
setCreateFolderModalOpen(true)
}}
onRename={openRenameModal}
onMove={(item) => {
// Move işlevi henüz implement edilmedi
toast.push(<Notification type="info">Move özelliği yakında eklenecek</Notification>)
}}
onDelete={(item) => openDeleteModal([item])}
onDownload={item.type === 'file' ? handleDownload : undefined}
onPreview={item.type === 'file' ? (item) => {
// Preview işlevi - resimler için modal açabiliriz
if (item.mimeType?.startsWith('image/')) {
// Resim preview modal'ıılabilir
toast.push(<Notification type="info">Resim önizleme özelliği yakında eklenecek</Notification>)
} else {
// Diğer dosya tipleri için download
handleDownload(item)
}
} : undefined}
/>
))
)}
</div>
</>
)}
{/* Modals */}
<FileUploadModal
isOpen={uploadModalOpen}
onClose={() => setUploadModalOpen(false)}
onUpload={handleUploadFiles}
currentFolderId={currentFolderId}
loading={uploading}
/>
<CreateFolderModal
isOpen={createFolderModalOpen}
onClose={() => setCreateFolderModalOpen(false)}
onCreate={handleCreateFolder}
loading={creating}
currentFolderId={currentFolderId}
/>
<RenameItemModal
isOpen={renameModalOpen}
onClose={() => {
setRenameModalOpen(false)
setItemToRename(undefined)
}}
onRename={handleRenameItem}
item={itemToRename}
loading={renaming}
/>
<DeleteConfirmModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false)
setItemsToDelete([])
}}
onDelete={handleDeleteItems}
items={itemsToDelete}
loading={deleting}
/>
</Container>
)
}
export default FileManager