336 lines
10 KiB
TypeScript
336 lines
10 KiB
TypeScript
import { getFiles, uploadFiles } from '@/api/Admin'
|
|
import { get } from '@/rtk/helpers/apiService'
|
|
import { getAccessToken } from '@/rtk/localStorage'
|
|
import {
|
|
Box,
|
|
Button,
|
|
Card,
|
|
Collapse,
|
|
Group,
|
|
Modal,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
} from '@mantine/core'
|
|
import { notifications } from '@mantine/notifications'
|
|
import {
|
|
IconChevronDown,
|
|
IconDownload,
|
|
IconFileTypeDocx,
|
|
IconFileTypePdf,
|
|
IconFolder,
|
|
IconListCheck,
|
|
IconPhoto,
|
|
IconSearch,
|
|
IconTrash,
|
|
} from '@tabler/icons-react'
|
|
import axios from 'axios'
|
|
import { useEffect, useState } from 'react'
|
|
import FileUploadForm from '../Profile/components/FileUploadForm'
|
|
import classes from './AllProfiles.module.css'
|
|
|
|
interface FileData {
|
|
id: number
|
|
name: string
|
|
url: string
|
|
type: string
|
|
description?: string
|
|
created_at: string
|
|
}
|
|
|
|
interface GroupedFiles {
|
|
[key: string]: FileData[]
|
|
}
|
|
|
|
const AllProfiles = () => {
|
|
const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
|
|
const [currentUser, setCurrentUser] = useState<string>('')
|
|
const [openedProfile, setOpenedProfile] = useState(false)
|
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [expandedFolders, setExpandedFolders] = useState<{
|
|
[key: string]: boolean
|
|
}>({})
|
|
const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
|
|
|
|
const toggleFolder = (userName: string) => {
|
|
setExpandedFolders((prev) => ({
|
|
...prev,
|
|
[userName]: !prev[userName],
|
|
}))
|
|
}
|
|
|
|
const getFileIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'document':
|
|
return <IconFileTypeDocx size={16} />
|
|
case 'image':
|
|
return <IconPhoto size={16} />
|
|
default:
|
|
return <IconFileTypePdf size={16} />
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (
|
|
e: React.FormEvent,
|
|
fileName: string,
|
|
description: string,
|
|
currentUser: string
|
|
) => {
|
|
e.preventDefault()
|
|
setIsLoading(true)
|
|
const formData = new FormData()
|
|
if (selectedFile) {
|
|
formData.append('file', selectedFile)
|
|
formData.append('name', fileName)
|
|
formData.append('description', description)
|
|
formData.append('user_name', currentUser)
|
|
|
|
try {
|
|
const token = await getAccessToken()
|
|
const response = await axios.post(uploadFiles, formData, {
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
if (response.status === 200) {
|
|
setSelectedFile(null)
|
|
await getAllFiles()
|
|
return true
|
|
}
|
|
return false
|
|
} catch (error) {
|
|
console.error('Error uploading file:', error)
|
|
throw error
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
const getAllFiles = async () => {
|
|
try {
|
|
const res = await get(getFiles)
|
|
if (res.status === true) {
|
|
setGroupedFiles(res.data)
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
const removeFile = async (id: number) => {
|
|
try {
|
|
const token = await getAccessToken();
|
|
const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
notifications.show({
|
|
title: 'Thành công',
|
|
message: 'Xóa file thành công',
|
|
color: 'green',
|
|
});
|
|
await getAllFiles();
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
notifications.show({
|
|
title: 'Lỗi',
|
|
message: 'Không thể xóa file',
|
|
color: 'red',
|
|
});
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
getAllFiles()
|
|
}, [])
|
|
|
|
const filterFiles = (files: FileData[], searchTerm: string) => {
|
|
return files.filter(
|
|
(file) =>
|
|
file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(file.description &&
|
|
file.description.toLowerCase().includes(searchTerm.toLowerCase())),
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className={classes.title}>
|
|
<h3>
|
|
<Text size="sm">Admin/</Text>
|
|
Files Management
|
|
</h3>
|
|
</div>
|
|
<Box ml={'md'}>
|
|
<Stack gap="xs">
|
|
{Object.entries(groupedFiles).map(([userName, files]) => (
|
|
<Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
|
|
<Group
|
|
justify="space-between"
|
|
mb="xs"
|
|
gap="xs"
|
|
onClick={() => toggleFolder(userName)}
|
|
style={{ cursor: 'pointer' }}
|
|
>
|
|
<Group gap="xs">
|
|
<IconFolder size={18} color="var(--mantine-color-yellow-9)" />
|
|
<Title order={5}>{userName}</Title>
|
|
</Group>
|
|
<Group gap="xs">
|
|
<Button
|
|
size="xs"
|
|
variant="subtle"
|
|
color="gray"
|
|
onClick={() => toggleFolder(userName)}
|
|
leftSection={
|
|
<IconChevronDown
|
|
size={14}
|
|
style={{
|
|
transform: expandedFolders[userName]
|
|
? 'rotate(180deg)'
|
|
: 'none',
|
|
transition: 'transform 0.2s',
|
|
}}
|
|
/>
|
|
}
|
|
>
|
|
{expandedFolders[userName] ? 'Collapse' : 'Expand'}
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
color="blue"
|
|
onClick={() => {
|
|
setCurrentUser(userName)
|
|
setOpenedProfile(true)
|
|
}}
|
|
>
|
|
<Group gap={2}>
|
|
<IconListCheck size={14} />
|
|
<Text size="xs">Upload Files</Text>
|
|
</Group>
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
<Collapse in={expandedFolders[userName]}>
|
|
<Stack gap="xs">
|
|
<TextInput
|
|
placeholder="Search files by name or description..."
|
|
size="xs"
|
|
leftSection={<IconSearch size={14} />}
|
|
value={searchTerms[userName] || ''}
|
|
onChange={(e) =>
|
|
setSearchTerms((prev) => ({
|
|
...prev,
|
|
[userName]: e.target.value,
|
|
}))
|
|
}
|
|
onClick={(e) => e.stopPropagation()}
|
|
/>
|
|
{filterFiles(files, searchTerms[userName] || '')
|
|
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
.map((file: FileData) => (
|
|
<Card
|
|
key={file.id}
|
|
shadow="xs"
|
|
padding="xs"
|
|
radius="sm"
|
|
withBorder
|
|
>
|
|
<Group justify="space-between" gap="xs">
|
|
<Group gap="xs">
|
|
{getFileIcon(file.type)}
|
|
<Box>
|
|
<Text size="xs" fw={500}>
|
|
{file.name}
|
|
</Text>
|
|
{file.description && (
|
|
<Text size="xs" c="dimmed">
|
|
{file.description}
|
|
</Text>
|
|
)}
|
|
<Text size="xs" c="dimmed">
|
|
Uploaded:{' '}
|
|
{new Date(file.created_at).toLocaleDateString()}
|
|
</Text>
|
|
</Box>
|
|
</Group>
|
|
<Group gap="xs">
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
color="blue"
|
|
component='a'
|
|
href={`${import.meta.env.VITE_BACKEND_URL}${
|
|
import.meta.env.VITE_BACKEND_URL?.includes(
|
|
'localhost',
|
|
)
|
|
? ''
|
|
: 'image/'
|
|
}${file.url.slice(1)}`}
|
|
target="_blank"
|
|
>
|
|
<Group gap={2}>
|
|
<IconDownload size={12} />
|
|
<Text size="xs">Download</Text>
|
|
</Group>
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
color="red"
|
|
onClick={() => removeFile(file.id)}
|
|
>
|
|
<Group gap={2}>
|
|
<IconTrash size={12} />
|
|
<Text size="xs">Delete</Text>
|
|
</Group>
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
</Card>
|
|
),
|
|
)}
|
|
</Stack>
|
|
</Collapse>
|
|
</Card>
|
|
))}
|
|
</Stack>
|
|
|
|
<Modal
|
|
size="lg"
|
|
opened={openedProfile}
|
|
onClose={() => {
|
|
setOpenedProfile(false)
|
|
setCurrentUser('')
|
|
setSelectedFile(null)
|
|
}}
|
|
>
|
|
<Box>
|
|
<FileUploadForm
|
|
data={groupedFiles[currentUser] || []}
|
|
handleSubmit={handleSubmit}
|
|
handleFileChange={(file) => file && setSelectedFile(file)}
|
|
removeFile={removeFile}
|
|
isLoading={isLoading}
|
|
currentUser={currentUser}
|
|
/>
|
|
</Box>
|
|
</Modal>
|
|
</Box>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AllProfiles
|