diff --git a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php
index d32762b..e1c812e 100644
--- a/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php
+++ b/BACKEND/Modules/Admin/app/Http/Controllers/ProfileController.php
@@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
use App\Traits\HasOrderByRequest;
use App\Traits\HasSearchRequest;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Log;
+use Modules\Admin\app\Models\Admin;
use Modules\Admin\app\Models\Sprint;
use Modules\Admin\app\Models\UserCriteria;
+use App\Models\Files;
+use App\DataTransferObjects\FileData;
class ProfileController extends Controller
{
@@ -28,6 +33,7 @@ class ProfileController extends Controller
$this->jiraService = $jiraService;
}
+
public function getProfilesData(Request $request)
{
$user = auth('admins')->user();
@@ -125,7 +131,7 @@ class ProfileController extends Controller
$rootFolder = rtrim($rootFolder, '/') . '/';
// Get all files and directories in the specified root folder
- $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder);
+ $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
return response()->json(['data' => $fileList, 'status' => true]);
}
@@ -185,6 +191,10 @@ class ProfileController extends Controller
if (!Storage::disk('public')->exists($othersDirectory)) {
Storage::disk('public')->makeDirectory($othersDirectory);
}
+
+ $adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
+ $currentUser = auth('admins')->user();
+
if ($request->hasFile('files')) {
foreach ($request->file('files') as $file) {
// Store the file and get its path
@@ -197,6 +207,32 @@ class ProfileController extends Controller
$path = $file->storeAs($baseDirectory, $originalFilename, 'public');
}
$uploadedFiles[] = $path;
+
+ // Tạo URL đầy đủ cho file
+ $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
+ ? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path))
+ : env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
+
+ // // Gửi email thông báo cho admin
+ // foreach ($adminEmails as $adminEmail) {
+ // $admin = Admin::where('email', $adminEmail)->first();
+ // if ($admin) {
+ // $this->sendFileUploadNotification(
+ // $admin,
+ // "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
+ // $fileUrl,
+ // "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
+ // );
+ // }
+ // }
+
+ // // Gửi email xác nhận cho người tải lên
+ // $this->sendFileUploadNotification(
+ // $currentUser,
+ // "Bạn đã tải lên file {$originalFilename} thành công",
+ // $fileUrl,
+ // "[APAC Tech] {$currentUser->name} - Tải file thành công"
+ // );
}
}
@@ -237,4 +273,195 @@ class ProfileController extends Controller
'message' => 'File not found',
], 404);
}
+
+ public function sendFileUploadNotification($user, $description, $url, $subject, $note)
+ {
+ try {
+ // Gửi email bất đồng bộ không cần job
+ dispatch(function() use ($user, $description, $url, $subject, $note) {
+ Mail::send('emails.file_upload_notification', [
+ 'user' => $user,
+ 'description' => $description,
+ 'url' => $url,
+ 'note' => $note
+ ], function ($message) use ($user, $subject) {
+ $message->to($user->email)
+ ->subject($subject);
+ });
+ })->afterResponse();
+
+ return true;
+ } catch (\Exception $e) {
+ Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
+ return false;
+ }
+ }
+
+ public function uploadFiles(Request $request)
+ {
+ try {
+ $request->validate([
+ 'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
+ 'name' => 'required|string|max:255',
+ 'description' => 'nullable|string',
+ 'user_name' => 'required|string|max:255'
+ ]);
+
+ $file = $request->file('file');
+ $user = auth('admins')->user();
+
+ // Tạo thư mục cho user nếu chưa tồn tại
+ $userFolder = 'files/' . $request->user_name;
+ if (!Storage::disk('public')->exists($userFolder)) {
+ Storage::disk('public')->makeDirectory($userFolder);
+ }
+
+ $path = $file->store($userFolder, 'public');
+
+ $fileRecord = Files::create([
+ 'name' => $request->name,
+ 'url' => $path,
+ 'type' => $this->getFileType($file->getClientOriginalName()),
+ 'description' => $request->description,
+ 'user_id' => Admin::where('name', $request->user_name)->first()->id
+ ]);
+
+ $currentUser = Admin::where('name', $request->user_name)->first();
+ // Gửi email thông báo cho người upload
+ $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production')
+ ? env('APP_URL') . '/image' . Storage::url($path)
+ : env('APP_URL') . Storage::url($path);
+ $this->sendFileUploadNotification(
+ $user,
+ 'Bạn đã tải lên file "' . $request->name . '" thành công',
+ $fileUrl,
+ "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
+ $request->description ?? 'No description'
+ );
+
+ // Gửi email thông báo cho tất cả admin khác
+ $otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
+ foreach ($otherAdmins as $admin) {
+ $this->sendFileUploadNotification(
+ $admin,
+ 'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
+ $fileUrl,
+ "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
+ $request->description ?? 'No description'
+ );
+ }
+
+ return response()->json([
+ 'status' => true,
+ 'message' => 'File uploaded successfully',
+ 'data' => [
+ 'id' => $fileRecord->id,
+ 'name' => $fileRecord->name,
+ 'url' => Storage::url($path),
+ 'type' => $fileRecord->type,
+ 'description' => $fileRecord->description
+ ]
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => false,
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ public function getFiles()
+ {
+ try {
+ $files = Files::with('user')->get()
+ ->map(function($file) {
+ return [
+ 'id' => $file->id,
+ 'name' => $file->name,
+ 'url' => Storage::url($file->url),
+ 'type' => $file->type,
+ 'description' => $file->description,
+ 'created_at' => $file->created_at,
+ 'user_id' => $file->user_id,
+ 'user_name' => $file->user->name
+ ];
+ });
+
+ // Gom nhóm files theo tên user
+ $groupedFiles = $files->groupBy('user_name')
+ ->map(function($files) {
+ return $files->map(function(array $file) {
+ return (object)[
+ 'id' => $file['id'],
+ 'name' => $file['name'],
+ 'url' => $file['url'],
+ 'type' => $file['type'],
+ 'description' => $file['description'],
+ 'created_at' => $file['created_at'],
+ 'user_id' => $file['user_id']
+ ];
+ });
+ });
+
+ return response()->json([
+ 'status' => true,
+ 'data' => $groupedFiles
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => false,
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ public function deleteFile($id)
+ {
+ try {
+ $file = Files::findOrFail($id);
+ $user = auth('admins')->user();
+
+ if ($file->user_id !== $user->id) {
+ return response()->json([
+ 'status' => false,
+ 'message' => 'Unauthorized'
+ ], 403);
+ }
+
+ Storage::disk('public')->delete($file->url);
+ $file->delete();
+
+ return response()->json([
+ 'status' => true,
+ 'message' => 'File deleted successfully'
+ ]);
+
+ } catch (\Exception $e) {
+ return response()->json([
+ 'status' => false,
+ 'message' => $e->getMessage()
+ ], 500);
+ }
+ }
+
+ private function getFileType($filename)
+ {
+ $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
+
+ $typeMap = [
+ 'pdf' => 'document',
+ 'doc' => 'document',
+ 'docx' => 'document',
+ 'jpg' => 'image',
+ 'jpeg' => 'image',
+ 'png' => 'image',
+ 'xls' => 'spreadsheet',
+ 'xlsx' => 'spreadsheet',
+ 'csv' => 'spreadsheet'
+ ];
+
+ return $typeMap[$extension] ?? 'other';
+ }
}
diff --git a/BACKEND/Modules/Admin/routes/api.php b/BACKEND/Modules/Admin/routes/api.php
index b561918..8f9ee2c 100755
--- a/BACKEND/Modules/Admin/routes/api.php
+++ b/BACKEND/Modules/Admin/routes/api.php
@@ -174,6 +174,9 @@ Route::middleware('api')
Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
+ Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
+ Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
+ Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
});
Route::group([
diff --git a/BACKEND/app/Models/Files.php b/BACKEND/app/Models/Files.php
new file mode 100644
index 0000000..64be814
--- /dev/null
+++ b/BACKEND/app/Models/Files.php
@@ -0,0 +1,25 @@
+belongsTo(User::class);
+ }
+}
diff --git a/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php b/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php
new file mode 100644
index 0000000..0951ec1
--- /dev/null
+++ b/BACKEND/database/migrations/2025_03_27_131933_create_files_table.php
@@ -0,0 +1,32 @@
+id();
+ $table->string('name');
+ $table->string('url');
+ $table->string('type');
+ $table->text('description')->nullable();
+ $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('files');
+ }
+};
diff --git a/BACKEND/database/seeders/DatabaseSeeder.php b/BACKEND/database/seeders/DatabaseSeeder.php
index a9f4519..c067f12 100755
--- a/BACKEND/database/seeders/DatabaseSeeder.php
+++ b/BACKEND/database/seeders/DatabaseSeeder.php
@@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
+
+ $this->call([
+ UserSeeder::class,
+ FileSeeder::class,
+ ]);
}
}
diff --git a/BACKEND/resources/views/emails/file_upload_notification.blade.php b/BACKEND/resources/views/emails/file_upload_notification.blade.php
new file mode 100644
index 0000000..3bbad65
--- /dev/null
+++ b/BACKEND/resources/views/emails/file_upload_notification.blade.php
@@ -0,0 +1,72 @@
+
+
+
+
+ Thông báo tải lên file mới
+
+
+
+
+
+
+
+
Xin chào {{ $user->name }},
+
+
{{ $description }} ở hệ thống APAC Tech.
+
Note: {{ $note }}
+
Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:
+
+
+
+
Trân trọng,
Đội ngũ APAC Tech
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FRONTEND/src/api/Admin.ts b/FRONTEND/src/api/Admin.ts
index ef404fe..b1c613f 100755
--- a/FRONTEND/src/api/Admin.ts
+++ b/FRONTEND/src/api/Admin.ts
@@ -119,3 +119,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
// Download File
export const downloadFile = API_URL + 'v1/admin/download-file'
+
+// Files APIs
+export const getFiles = API_URL + 'v1/admin/profile/files'
+export const uploadFiles = API_URL + 'v1/admin/profile/upload-files'
+export const deleteFileById = API_URL + 'v1/admin/profile/files'
diff --git a/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx b/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx
index 1893703..9873dda 100644
--- a/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx
+++ b/FRONTEND/src/pages/AllProfiles/AllProfiles.tsx
@@ -1,498 +1,330 @@
-import {
- deleteFile,
- getAllFilesInProfiles,
- updateProfileFolder,
-} from '@/api/Admin'
-import { Xdelete } from '@/rtk/helpers/CRUD'
+import { getFiles, uploadFiles } from '@/api/Admin'
import { get } from '@/rtk/helpers/apiService'
import { getAccessToken } from '@/rtk/localStorage'
import {
Box,
Button,
- FileInput,
+ Card,
+ Collapse,
Group,
Modal,
- RenderTreeNodePayload,
Stack,
Text,
TextInput,
- Tooltip,
- Tree,
+ Title,
} from '@mantine/core'
+import { notifications } from '@mantine/notifications'
import {
+ IconChevronDown,
+ IconDownload,
IconFileTypeDocx,
IconFileTypePdf,
IconFolder,
- IconFolderOpen,
- IconFolderX,
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 FileIconProps {
+interface FileData {
+ id: number
name: string
- isFolder: boolean
- expanded: boolean
+ url: string
+ type: string
+ description?: string
+ created_at: string
}
-type TFileProfile = {
- label: string
- type: string
- value: string
- children?: TFileProfile[]
+interface GroupedFiles {
+ [key: string]: FileData[]
}
const AllProfiles = () => {
- const [treeData, setTreeData] = useState([])
- const [cv, setCv] = useState()
- const [idCard, setIdCard] = useState()
- const [transcript, setTranscript] = useState()
- const [universityDiploma, setUniversityDiploma] = useState()
- const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
- const [data, setData] = useState([])
- const [currentName, setCurrentName] = useState('')
+ const [groupedFiles, setGroupedFiles] = useState({})
+ const [currentUser, setCurrentUser] = useState('')
const [openedProfile, setOpenedProfile] = useState(false)
- function FileIcon({ name, isFolder, expanded }: FileIconProps) {
- if (name.endsWith('.pdf')) {
- return
- }
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [expandedFolders, setExpandedFolders] = useState<{
+ [key: string]: boolean
+ }>({})
+ const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
- if (name.endsWith('.doc') || name.endsWith('.docx')) {
- return
- }
-
- if (
- name.endsWith('.jpg') ||
- name.endsWith('.png') ||
- name.endsWith('.jpeg') ||
- name.endsWith('.webp')
- ) {
- return
- }
-
- if (isFolder) {
- return expanded ? (
-
- ) : (
-
- )
- }
-
- return (
-
- )
+ const toggleFolder = (userName: string) => {
+ setExpandedFolders((prev) => ({
+ ...prev,
+ [userName]: !prev[userName],
+ }))
}
- function Leaf({
- node,
- expanded,
- hasChildren,
- elementProps,
- }: RenderTreeNodePayload) {
- return (
-
- {!node.children ? (
-
-
- {node.label}
-
- ) : (
- <>
-
- {node.label}
-
- {
- setCurrentName(node.label!.toString())
- setOpenedProfile(true)
- }}
- />
-
- >
- )}
-
- )
+ const getFileIcon = (type: string) => {
+ switch (type) {
+ case 'document':
+ return
+ case 'image':
+ return
+ default:
+ return
+ }
}
- const handleOtherFileChange = (
- index: number,
- field: string,
- value: File | string,
+ const handleSubmit = async (
+ e: React.FormEvent,
+ fileName: string,
+ description: string,
+ currentUser: string
) => {
- const updatedFiles: any = [...otherFiles]
- updatedFiles[index][field] = value
- setOtherFiles(updatedFiles)
- }
-
- const addOtherFileInput = () => {
- setOtherFiles([...otherFiles, { file: null, type: '' }])
- }
-
- const handleSubmit = async (e: any) => {
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)
- // Append each selected file to FormData
- for (let i = 0; i < otherFiles.length; i++) {
- if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
- formData.append(
- 'files[]',
- handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
- )
+ 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
+ }
- if (cv) {
- formData.append('files[]', cv)
- }
-
- if (idCard) {
- formData.append('files[]', idCard)
- }
-
- if (transcript) {
- formData.append('files[]', transcript)
- }
-
- if (universityDiploma) {
- formData.append('files[]', universityDiploma)
- }
-
- formData.append('name', currentName)
-
- const token = await getAccessToken()
+ const getAllFiles = async () => {
try {
- const response = await axios.post(updateProfileFolder, formData, {
+ 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: {
- 'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${token}`,
},
- })
+ });
if (response.status === 200) {
- getAllFile()
- getTree()
- setOtherFiles([])
+ notifications.show({
+ title: 'Thành công',
+ message: 'Xóa file thành công',
+ color: 'green',
+ });
+ await getAllFiles();
}
} catch (error) {
- console.error('Error uploading files', error)
+ console.log(error);
+ notifications.show({
+ title: 'Lỗi',
+ message: 'Không thể xóa file',
+ color: 'red',
+ });
}
}
- const getAllFile = async () => {
- try {
- const res = await get(getAllFilesInProfiles, {
- root_folder: '/storage/profiles/' + currentName,
- })
- if (res.status === true) {
- setData(res.data)
- }
- } catch (error) {
- console.log(error)
- }
- }
-
- const removeFile = async (url: string) => {
- try {
- await Xdelete(deleteFile, { file_url: url }, getAllFile)
- getTree()
- } catch (error) {
- console.log(error)
- }
- }
- const getTree = async () => {
- try {
- const res = await get(getAllFilesInProfiles, {
- root_folder: '/storage/profiles',
- })
- if (res.status === true) {
- setTreeData(res.data)
- }
- } catch (error) {
- console.log(error)
- }
- }
-
- const handleChangeFileName = (e: File, newName: string) => {
- const originalFile = e // Get the original file
- const extend = originalFile.name.split('.')[1]
- if (originalFile) {
- const newFileName = `${newName}.${extend}` // Create new file name
- const newFile = new File([originalFile], newFileName, {
- type: originalFile.type,
- }) // Create new file object
-
- return newFile // Save the new file object for further processing
- }
- }
-
- const checkFileExist = (nameField: string) => {
- const file = data.find((f) => f.label.includes(nameField))
- return file
- }
-
useEffect(() => {
- getTree()
+ getAllFiles()
}, [])
- useEffect(() => {
- getAllFile()
- }, [currentName])
+ 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 (
- Admin/
- Profiles
+ Admin/
+ Files Management
-
- }
- />
+
+
+ {Object.entries(groupedFiles).map(([userName, files]) => (
+
+ toggleFolder(userName)}
+ style={{ cursor: 'pointer' }}
+ >
+
+
+ {userName}
+
+
+
+
+
+
+
+
+ }
+ 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) => (
+
+
+
+ {getFileIcon(file.type)}
+
+
+ {file.name}
+
+ {file.description && (
+
+ {file.description}
+
+ )}
+
+ Uploaded:{' '}
+ {new Date(file.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
+ ),
+ )}
+
+
+
+ ))}
+
+
{
setOpenedProfile(false)
+ setCurrentUser('')
+ setSelectedFile(null)
}}
>
-
+ file && setSelectedFile(file)}
+ removeFile={removeFile}
+ isLoading={isLoading}
+ currentUser={currentUser}
+ />
diff --git a/FRONTEND/src/pages/Profile/Profile.tsx b/FRONTEND/src/pages/Profile/Profile.tsx
index 14c6210..e839e65 100644
--- a/FRONTEND/src/pages/Profile/Profile.tsx
+++ b/FRONTEND/src/pages/Profile/Profile.tsx
@@ -1,18 +1,16 @@
import {
- deleteFile,
- getAllFilesInProfiles,
+ getFiles,
getProfilesData,
listUserTechnical,
- updateProfileFolder,
updateProfilesData,
updateUserTechnical,
+ uploadFiles
} from '@/api/Admin'
import { changePassword } from '@/api/Auth'
import DataTableAll from '@/components/DataTable/DataTable'
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
import { logout } from '@/rtk/dispatches/auth'
-import { Xdelete } from '@/rtk/helpers/CRUD'
import { get, post, postImage } from '@/rtk/helpers/apiService'
import { requirementsPassword } from '@/rtk/helpers/variables'
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
@@ -22,16 +20,13 @@ import {
Avatar,
Box,
Button,
- FileInput,
Flex,
- Group,
Loader,
Modal,
PasswordInput,
- Stack,
Text,
TextInput,
- Title,
+ Title
} from '@mantine/core'
import { notifications } from '@mantine/notifications'
import {
@@ -46,15 +41,26 @@ import moment from 'moment'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import classes from './Profile.module.css'
+import FileUploadForm from './components/FileUploadForm'
const isCompactMenu = false
-type TFileProfile = {
- label: string
- type: string
- value: string
- children?: TFileProfile[]
+// type TFileProfile = {
+// label: string
+// type: string
+// value: string
+// children?: TFileProfile[]
+// }
+
+interface FileData {
+ id: number;
+ name: string;
+ url: string;
+ type: string;
+ description?: string;
+ created_at: string;
}
+
const Profile = () => {
const user = useAppSelector((state) => state.authentication.user)
const userData = getUser()
@@ -77,6 +83,11 @@ const Profile = () => {
const navigate = useNavigate()
const dispatch = useAppDispatch()
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+ const [data, setData] = useState([])
+ const [openedProfile, setOpenedProfile] = useState(false)
+
const updateAvatar = async (file: File) => {
try {
const res = await postImage(updateProfilesData, file, 'post')
@@ -137,102 +148,113 @@ const Profile = () => {
return []
}
- const [cv, setCv] = useState()
- const [idCard, setIdCard] = useState()
- const [transcript, setTranscript] = useState()
- const [universityDiploma, setUniversityDiploma] = useState()
- const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
- const [data, setData] = useState([])
- const [openedProfile, setOpenedProfile] = useState(false)
- const handleOtherFileChange = (
- index: number,
- field: string,
- value: File | string,
+ // const [cv, setCv] = useState()
+ // const [idCard, setIdCard] = useState()
+ // const [transcript, setTranscript] = useState()
+ // const [universityDiploma, setUniversityDiploma] = useState()
+ // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
+ // const handleOtherFileChange = (
+ // index: number,
+ // field: string,
+ // value: File | string,
+ // ) => {
+ // const updatedFiles: any = [...otherFiles]
+ // updatedFiles[index][field] = value
+ // setOtherFiles(updatedFiles)
+ // }
+
+ // const addOtherFileInput = () => {
+ // setOtherFiles([...otherFiles, { file: null, type: '' }])
+ // }
+
+ const handleSubmit = async (
+ e: React.FormEvent,
+ fileName: string,
+ description: string,
) => {
- const updatedFiles: any = [...otherFiles]
- updatedFiles[index][field] = value
- setOtherFiles(updatedFiles)
- }
-
- const addOtherFileInput = () => {
- setOtherFiles([...otherFiles, { file: null, type: '' }])
- }
-
- const handleSubmit = async (e: any) => {
e.preventDefault()
+ setIsLoading(true)
const formData = new FormData()
- // Append each selected file to FormData
- for (let i = 0; i < otherFiles.length; i++) {
- if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
- formData.append(
- 'files[]',
- handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
- )
+ if (selectedFile) {
+ formData.append('file', selectedFile)
+ formData.append('name', fileName)
+ formData.append('description', description)
+ formData.append('user_name', user.user.name)
+ formData.append('user_id', user.user.id.toString())
+
+ 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) {
+ await getAllFile()
+ setSelectedFile(null)
+ }
+ } catch (error) {
+ console.error('Error uploading file:', error)
+ } finally {
+ setIsLoading(false)
}
}
-
- if (cv) {
- formData.append('files[]', cv)
- }
-
- if (idCard) {
- formData.append('files[]', idCard)
- }
-
- if (transcript) {
- formData.append('files[]', transcript)
- }
-
- if (universityDiploma) {
- formData.append('files[]', universityDiploma)
- }
-
- const token = await getAccessToken()
- try {
- const response = await axios.post(updateProfileFolder, formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- Authorization: `Bearer ${token}`,
- },
- })
-
- if(response.status === 200){
- getAllFile()
- setOtherFiles([])
- }
- } catch (error) {
- console.error('Error uploading files', error)
- }
}
const getAllFile = async () => {
try {
- const res = await get(getAllFilesInProfiles, {
- root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
- })
+ const res = await get(getFiles)
if (res.status === true) {
- setData(res.data)
+ const userFiles = res.data[user.user.name] || [];
+ setData(userFiles);
}
} catch (error) {
console.log(error)
+ notifications.show({
+ title: 'Lỗi',
+ message: 'Không thể tải danh sách file',
+ color: 'red',
+ })
}
}
- const removeFile = async (url: string) => {
+ const removeFile = async (id: number) => {
try {
- await Xdelete(deleteFile, {file_url: url}, getAllFile)
+ 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 getAllFile();
+ }
} catch (error) {
- console.log(error)
+ console.log(error);
+ notifications.show({
+ title: 'Lỗi',
+ message: 'Không thể xóa file',
+ color: 'red',
+ });
}
- }
+ };
+
useEffect(() => {
const fetchData = async () => {
const result = await getListProfilesData()
setDataProfile(result ?? [])
+ await getAllFile()
}
fetchData()
- getAllFile()
}, [])
const handleChangePassword = async () => {
@@ -306,23 +328,19 @@ const Profile = () => {
dispatch(logout(navigate))
}, [dispatch, navigate])
- const handleChangeFileName = (e: File, newName: string) => {
- const originalFile = e // Get the original file
- const extend = originalFile.name.split('.')[1]
- if (originalFile) {
- const newFileName = `${newName}.${extend}` // Create new file name
- const newFile = new File([originalFile], newFileName, {
- type: originalFile.type,
- }) // Create new file object
+ // const handleChangeFileName = (e: File, newName: string): File => {
+ // const originalFile = e;
+ // const extend = originalFile.name.split('.')[1];
+ // const newFileName = `${newName}.${extend}`;
+ // return new File([originalFile], newFileName, {
+ // type: originalFile.type,
+ // });
+ // };
- return newFile // Save the new file object for further processing
- }
- }
-
- const checkFileExist = (nameField: string) => {
- const file = data.find((f) => f.label.includes(nameField))
- return file
- }
+ // const checkFileExist = (nameField: string) => {
+ // const file = data.find((f) => f.name.includes(nameField));
+ // return file;
+ // };
return (
@@ -578,162 +596,14 @@ const Profile = () => {
}}
>
-
+ file && setSelectedFile(file)}
+ removeFile={removeFile}
+ isLoading={isLoading}
+ currentUser={user.user.name}
+ />
diff --git a/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css b/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css
new file mode 100644
index 0000000..a02278b
--- /dev/null
+++ b/FRONTEND/src/pages/Profile/components/FileUploadForm.module.css
@@ -0,0 +1,121 @@
+.fileContainer {
+ padding: 12px;
+ margin: 8px 0;
+ border: 1px solid #e9ecef;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.fileContainer:hover {
+ background-color: #f8f9fa;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.fileHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.fileName {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 500;
+}
+
+.fileDescription {
+ font-size: 13px;
+ color: #666;
+ margin: 2px 0;
+}
+
+.fileActions {
+ display: flex;
+ gap: 8px;
+ margin-top: 4px;
+}
+
+.fileLink {
+ color: #228be6;
+ text-decoration: none;
+ font-size: 13px;
+ transition: color 0.2s ease;
+ padding: 4px 8px;
+ border-radius: 4px;
+ background-color: #e7f5ff;
+}
+
+.fileLink:hover {
+ color: #1c7ed6;
+ background-color: #d0ebff;
+}
+
+.deleteButton {
+ padding: 4px 8px;
+ font-size: 13px;
+}
+
+.fileInputGroup {
+ padding: 16px;
+ margin: 16px 0;
+ border: 2px dashed #e9ecef;
+ border-radius: 6px;
+ background-color: #f8f9fa;
+}
+
+.fileInput,
+.fileNameInput,
+.descriptionInput {
+ margin-bottom: 12px;
+}
+
+.saveButton {
+ margin-top: 16px;
+ width: 100%;
+ max-width: 180px;
+}
+
+.saveButton:disabled {
+ background-color: #e9ecef;
+ cursor: not-allowed;
+}
+
+.loadingOverlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(255, 255, 255, 0.8);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+}
+
+.loadingSpinner {
+ width: 32px;
+ height: 32px;Ø
+ border: 3px solid #f3f3f3;
+ border-top: 3px solid #3498db;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+.sectionTitle {
+ font-size: 16px;
+ font-weight: 600;
+ margin-bottom: 12px;
+ color: #343a40;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #e9ecef;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
\ No newline at end of file
diff --git a/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx b/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx
new file mode 100644
index 0000000..d8315e7
--- /dev/null
+++ b/FRONTEND/src/pages/Profile/components/FileUploadForm.tsx
@@ -0,0 +1,251 @@
+import {
+ Box,
+ Button,
+ Card,
+ FileInput,
+ Group,
+ Stack,
+ Text,
+ TextInput,
+ Textarea,
+} from '@mantine/core'
+import { notifications } from '@mantine/notifications'
+import {
+ IconDownload,
+ IconFileTypeDocx,
+ IconFileTypePdf,
+ IconFileTypeXls,
+ IconPhoto,
+ IconSearch,
+ IconTrash,
+} from '@tabler/icons-react'
+import { useState } from 'react'
+import classes from './FileUploadForm.module.css'
+
+// type TFileProfile = {
+// label: string
+// type: string
+// value: string
+// children?: TFileProfile[]
+// }
+
+interface FileData {
+ id: number
+ name: string
+ url: string
+ type: string
+ description?: string
+ created_at: string
+}
+
+type FileUploadFormProps = {
+ data: FileData[];
+ handleSubmit: (e: React.FormEvent, fileName: string, description: string, currentUser: string) => Promise
;
+ handleFileChange: (file: File | null) => void;
+ removeFile: (id: number) => Promise;
+ isLoading: boolean;
+ currentUser: string;
+};
+
+const FileUploadForm = ({
+ data,
+ handleSubmit,
+ handleFileChange,
+ removeFile,
+ isLoading,
+ currentUser,
+}: FileUploadFormProps) => {
+ const [selectedFile, setSelectedFile] = useState(null)
+ const [fileName, setFileName] = useState('')
+ const [description, setDescription] = useState('')
+ const [isUploading, setIsUploading] = useState(false)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ const handleFileSelect = (file: File | null) => {
+ setSelectedFile(file)
+ handleFileChange(file)
+ if (file) {
+ // Set default name as file name without extension
+ setFileName(file.name.split('.')[0])
+ }
+ }
+
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setIsUploading(true)
+
+ try {
+ await handleSubmit(e, fileName, description, currentUser)
+ notifications.show({
+ title: 'Thành công',
+ message: 'Tải file lên thành công',
+ color: 'green',
+ })
+ setFileName('')
+ setDescription('')
+ setSelectedFile(null)
+ } catch (error) {
+ console.error('Error submitting form:', error)
+ notifications.show({
+ title: 'Lỗi',
+ message: 'Không thể tải file lên',
+ color: 'red',
+ })
+ } finally {
+ setIsUploading(false)
+ }
+ }
+
+ const getFileIcon = (type: string) => {
+ switch (type) {
+ case 'document':
+ return
+ case 'image':
+ return
+ case 'spreadsheet':
+ return
+ default:
+ return
+ }
+ }
+
+ const filteredFiles = data.filter(
+ (file) =>
+ file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ (file.description &&
+ file.description.toLowerCase().includes(searchTerm.toLowerCase())),
+ )
+
+ return (
+ <>
+ {isLoading && (
+
+ )}
+
+
+ >
+ )
+}
+
+export default FileUploadForm