truong-leave-day #116
			
				
			
		
		
		
	| 
						 | 
					@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
 | 
				
			||||||
use App\Traits\HasOrderByRequest;
 | 
					use App\Traits\HasOrderByRequest;
 | 
				
			||||||
use App\Traits\HasSearchRequest;
 | 
					use App\Traits\HasSearchRequest;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Mail;
 | 
				
			||||||
use Illuminate\Support\Facades\Storage;
 | 
					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\Sprint;
 | 
				
			||||||
use Modules\Admin\app\Models\UserCriteria;
 | 
					use Modules\Admin\app\Models\UserCriteria;
 | 
				
			||||||
 | 
					use App\Models\Files;
 | 
				
			||||||
 | 
					use App\DataTransferObjects\FileData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProfileController extends Controller
 | 
					class ProfileController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					@ -28,6 +33,7 @@ class ProfileController extends Controller
 | 
				
			||||||
        $this->jiraService = $jiraService;
 | 
					        $this->jiraService = $jiraService;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getProfilesData(Request $request)
 | 
					    public function getProfilesData(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $user = auth('admins')->user();
 | 
					        $user = auth('admins')->user();
 | 
				
			||||||
| 
						 | 
					@ -125,7 +131,7 @@ class ProfileController extends Controller
 | 
				
			||||||
        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
					        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Get all files and directories in the specified root folder
 | 
					        // 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]);
 | 
					        return response()->json(['data' => $fileList, 'status' => true]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -185,6 +191,10 @@ class ProfileController extends Controller
 | 
				
			||||||
        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
					        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
				
			||||||
            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
					            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
 | 
				
			||||||
 | 
					        $currentUser = auth('admins')->user();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($request->hasFile('files')) {
 | 
					        if ($request->hasFile('files')) {
 | 
				
			||||||
            foreach ($request->file('files') as $file) {
 | 
					            foreach ($request->file('files') as $file) {
 | 
				
			||||||
                // Store the file and get its path
 | 
					                // Store the file and get its path
 | 
				
			||||||
| 
						 | 
					@ -197,6 +207,32 @@ class ProfileController extends Controller
 | 
				
			||||||
                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
					                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                $uploadedFiles[] = $path;
 | 
					                $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',
 | 
					            'message' => 'File not found',
 | 
				
			||||||
        ], 404);
 | 
					        ], 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';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,6 +174,9 @@ Route::middleware('api')
 | 
				
			||||||
                Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
					                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::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('/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([
 | 
					            Route::group([
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,25 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Model;
 | 
				
			||||||
 | 
					use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Files extends Model
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    use HasFactory;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected $fillable = [
 | 
				
			||||||
 | 
					        'name',
 | 
				
			||||||
 | 
					        'url',
 | 
				
			||||||
 | 
					        'type',
 | 
				
			||||||
 | 
					        'description',
 | 
				
			||||||
 | 
					        'user_id'
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function user(): BelongsTo
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->belongsTo(User::class);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					return new class extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Run the migrations.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function up(): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::create('files', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->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');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -18,5 +18,10 @@ class DatabaseSeeder extends Seeder
 | 
				
			||||||
        //     'name' => 'Test User',
 | 
					        //     'name' => 'Test User',
 | 
				
			||||||
        //     'email' => 'test@example.com',
 | 
					        //     'email' => 'test@example.com',
 | 
				
			||||||
        // ]);
 | 
					        // ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->call([
 | 
				
			||||||
 | 
					            UserSeeder::class,
 | 
				
			||||||
 | 
					            FileSeeder::class,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html>
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					    <meta charset="utf-8">
 | 
				
			||||||
 | 
					    <title>Thông báo tải lên file mới</title>
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        body {
 | 
				
			||||||
 | 
					            font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					            line-height: 1.6;
 | 
				
			||||||
 | 
					            color: #333;
 | 
				
			||||||
 | 
					            margin: 0;
 | 
				
			||||||
 | 
					            padding: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .container {
 | 
				
			||||||
 | 
					            max-width: 600px;
 | 
				
			||||||
 | 
					            margin: 0 auto;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .header {
 | 
				
			||||||
 | 
					            background-color: #1a73e8;
 | 
				
			||||||
 | 
					            color: white;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .content {
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            background-color: #f9f9f9;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .button {
 | 
				
			||||||
 | 
					            display: inline-block;
 | 
				
			||||||
 | 
					            padding: 12px 24px;
 | 
				
			||||||
 | 
					            background-color: #1a73e8;
 | 
				
			||||||
 | 
					            color: white;
 | 
				
			||||||
 | 
					            text-decoration: none;
 | 
				
			||||||
 | 
					            border-radius: 4px;
 | 
				
			||||||
 | 
					            margin: 20px 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .footer {
 | 
				
			||||||
 | 
					            text-align: center;
 | 
				
			||||||
 | 
					            padding: 20px;
 | 
				
			||||||
 | 
					            background-color: #f5f5f5;
 | 
				
			||||||
 | 
					            font-size: 12px;
 | 
				
			||||||
 | 
					            color: #666;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					    <div class="container">
 | 
				
			||||||
 | 
					        <div class="header">
 | 
				
			||||||
 | 
					            <h1>Thông báo tải lên file mới</h1>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="content">
 | 
				
			||||||
 | 
					            <p>Xin chào {{ $user->name }},</p>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <p>{{ $description }} ở hệ thống APAC Tech.</p>
 | 
				
			||||||
 | 
					            <p>Note: {{ $note }}</p>
 | 
				
			||||||
 | 
					            <p>Vui lòng kiểm tra ngay thông tin bằng cách nhấn nút bên dưới:</p>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <div style="text-align: center;">
 | 
				
			||||||
 | 
					                <a href="{{ $url }}" class="button">Kiểm tra ngay</a>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            <p>Trân trọng,<br>Đội ngũ APAC Tech</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <div class="footer">
 | 
				
			||||||
 | 
					            <p>Email này được gửi tự động từ hệ thống APAC Tech. Vui lòng không trả lời email này.</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html> 
 | 
				
			||||||
| 
						 | 
					@ -119,3 +119,8 @@ export const deleteDocument = API_URL + 'v1/admin/document/delete'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Download File
 | 
					// Download File
 | 
				
			||||||
export const downloadFile = API_URL + 'v1/admin/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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,498 +1,330 @@
 | 
				
			||||||
import {
 | 
					import { getFiles, uploadFiles } from '@/api/Admin'
 | 
				
			||||||
  deleteFile,
 | 
					 | 
				
			||||||
  getAllFilesInProfiles,
 | 
					 | 
				
			||||||
  updateProfileFolder,
 | 
					 | 
				
			||||||
} from '@/api/Admin'
 | 
					 | 
				
			||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
					 | 
				
			||||||
import { get } from '@/rtk/helpers/apiService'
 | 
					import { get } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
					import { getAccessToken } from '@/rtk/localStorage'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Box,
 | 
					  Box,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  FileInput,
 | 
					  Card,
 | 
				
			||||||
 | 
					  Collapse,
 | 
				
			||||||
  Group,
 | 
					  Group,
 | 
				
			||||||
  Modal,
 | 
					  Modal,
 | 
				
			||||||
  RenderTreeNodePayload,
 | 
					 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Tooltip,
 | 
					  Title,
 | 
				
			||||||
  Tree,
 | 
					 | 
				
			||||||
} from '@mantine/core'
 | 
					} from '@mantine/core'
 | 
				
			||||||
 | 
					import { notifications } from '@mantine/notifications'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					  IconChevronDown,
 | 
				
			||||||
 | 
					  IconDownload,
 | 
				
			||||||
  IconFileTypeDocx,
 | 
					  IconFileTypeDocx,
 | 
				
			||||||
  IconFileTypePdf,
 | 
					  IconFileTypePdf,
 | 
				
			||||||
  IconFolder,
 | 
					  IconFolder,
 | 
				
			||||||
  IconFolderOpen,
 | 
					 | 
				
			||||||
  IconFolderX,
 | 
					 | 
				
			||||||
  IconListCheck,
 | 
					  IconListCheck,
 | 
				
			||||||
  IconPhoto,
 | 
					  IconPhoto,
 | 
				
			||||||
 | 
					  IconSearch,
 | 
				
			||||||
 | 
					  IconTrash,
 | 
				
			||||||
} from '@tabler/icons-react'
 | 
					} from '@tabler/icons-react'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import { useEffect, useState } from 'react'
 | 
					import { useEffect, useState } from 'react'
 | 
				
			||||||
 | 
					import FileUploadForm from '../Profile/components/FileUploadForm'
 | 
				
			||||||
import classes from './AllProfiles.module.css'
 | 
					import classes from './AllProfiles.module.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FileIconProps {
 | 
					interface FileData {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
  name: string
 | 
					  name: string
 | 
				
			||||||
  isFolder: boolean
 | 
					  url: string
 | 
				
			||||||
  expanded: boolean
 | 
					  type: string
 | 
				
			||||||
 | 
					  description?: string
 | 
				
			||||||
 | 
					  created_at: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TFileProfile = {
 | 
					interface GroupedFiles {
 | 
				
			||||||
  label: string
 | 
					  [key: string]: FileData[]
 | 
				
			||||||
  type: string
 | 
					 | 
				
			||||||
  value: string
 | 
					 | 
				
			||||||
  children?: TFileProfile[]
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const AllProfiles = () => {
 | 
					const AllProfiles = () => {
 | 
				
			||||||
  const [treeData, setTreeData] = useState([])
 | 
					  const [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
 | 
				
			||||||
  const [cv, setCv] = useState<File>()
 | 
					  const [currentUser, setCurrentUser] = useState<string>('')
 | 
				
			||||||
  const [idCard, setIdCard] = useState<File>()
 | 
					 | 
				
			||||||
  const [transcript, setTranscript] = useState<File>()
 | 
					 | 
				
			||||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
					 | 
				
			||||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
					 | 
				
			||||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
					 | 
				
			||||||
  const [currentName, setCurrentName] = useState<string>('')
 | 
					 | 
				
			||||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
					  const [openedProfile, setOpenedProfile] = useState(false)
 | 
				
			||||||
  function FileIcon({ name, isFolder, expanded }: FileIconProps) {
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
				
			||||||
    if (name.endsWith('.pdf')) {
 | 
					  const [isLoading, setIsLoading] = useState(false)
 | 
				
			||||||
      return <IconFileTypePdf size={14} />
 | 
					  const [expandedFolders, setExpandedFolders] = useState<{
 | 
				
			||||||
    }
 | 
					    [key: string]: boolean
 | 
				
			||||||
 | 
					  }>({})
 | 
				
			||||||
 | 
					  const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
					  const toggleFolder = (userName: string) => {
 | 
				
			||||||
      return <IconFileTypeDocx size={14} />
 | 
					    setExpandedFolders((prev) => ({
 | 
				
			||||||
    }
 | 
					      ...prev,
 | 
				
			||||||
 | 
					      [userName]: !prev[userName],
 | 
				
			||||||
    if (
 | 
					    }))
 | 
				
			||||||
      name.endsWith('.jpg') ||
 | 
					 | 
				
			||||||
      name.endsWith('.png') ||
 | 
					 | 
				
			||||||
      name.endsWith('.jpeg') ||
 | 
					 | 
				
			||||||
      name.endsWith('.webp')
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return <IconPhoto size={14} />
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isFolder) {
 | 
					 | 
				
			||||||
      return expanded ? (
 | 
					 | 
				
			||||||
        <IconFolderOpen
 | 
					 | 
				
			||||||
          color="var(--mantine-color-yellow-9)"
 | 
					 | 
				
			||||||
          size={14}
 | 
					 | 
				
			||||||
          stroke={2.5}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      ) : (
 | 
					 | 
				
			||||||
        <IconFolder
 | 
					 | 
				
			||||||
          color="var(--mantine-color-yellow-9)"
 | 
					 | 
				
			||||||
          size={14}
 | 
					 | 
				
			||||||
          stroke={2.5}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <IconFolderX color="var(--mantine-color-red-9)" size={14} stroke={2.5} />
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function Leaf({
 | 
					  const getFileIcon = (type: string) => {
 | 
				
			||||||
    node,
 | 
					    switch (type) {
 | 
				
			||||||
    expanded,
 | 
					      case 'document':
 | 
				
			||||||
    hasChildren,
 | 
					        return <IconFileTypeDocx size={16} />
 | 
				
			||||||
    elementProps,
 | 
					      case 'image':
 | 
				
			||||||
  }: RenderTreeNodePayload) {
 | 
					        return <IconPhoto size={16} />
 | 
				
			||||||
    return (
 | 
					      default:
 | 
				
			||||||
      <Group gap={5} {...elementProps}>
 | 
					        return <IconFileTypePdf size={16} />
 | 
				
			||||||
        {!node.children ? (
 | 
					    }
 | 
				
			||||||
          <a href={node.value} target="_blank">
 | 
					 | 
				
			||||||
            <FileIcon
 | 
					 | 
				
			||||||
              name={node.value}
 | 
					 | 
				
			||||||
              isFolder={hasChildren}
 | 
					 | 
				
			||||||
              expanded={expanded}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span>{node.label}</span>
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
        ) : (
 | 
					 | 
				
			||||||
          <>
 | 
					 | 
				
			||||||
            <FileIcon
 | 
					 | 
				
			||||||
              name={node.value}
 | 
					 | 
				
			||||||
              isFolder={hasChildren}
 | 
					 | 
				
			||||||
              expanded={expanded}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span>{node.label}</span>
 | 
					 | 
				
			||||||
            <Tooltip label="Upload">
 | 
					 | 
				
			||||||
              <IconListCheck
 | 
					 | 
				
			||||||
                color="green"
 | 
					 | 
				
			||||||
                width={15}
 | 
					 | 
				
			||||||
                height={15}
 | 
					 | 
				
			||||||
                style={{
 | 
					 | 
				
			||||||
                  display: node.label !== 'others' ? 'block' : 'none',
 | 
					 | 
				
			||||||
                  cursor: 'pointer',
 | 
					 | 
				
			||||||
                  zIndex: 100000,
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
                onClick={() => {
 | 
					 | 
				
			||||||
                  setCurrentName(node.label!.toString())
 | 
					 | 
				
			||||||
                  setOpenedProfile(true)
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            </Tooltip>
 | 
					 | 
				
			||||||
          </>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </Group>
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleOtherFileChange = (
 | 
					  const handleSubmit = async (
 | 
				
			||||||
    index: number,
 | 
					    e: React.FormEvent,
 | 
				
			||||||
    field: string,
 | 
					    fileName: string,
 | 
				
			||||||
    value: File | 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()
 | 
					    e.preventDefault()
 | 
				
			||||||
 | 
					    setIsLoading(true)
 | 
				
			||||||
    const formData = new FormData()
 | 
					    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
 | 
					      try {
 | 
				
			||||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
					        const token = await getAccessToken()
 | 
				
			||||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
					        const response = await axios.post(uploadFiles, formData, {
 | 
				
			||||||
        formData.append(
 | 
					          headers: {
 | 
				
			||||||
          'files[]',
 | 
					            'Content-Type': 'multipart/form-data',
 | 
				
			||||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
					            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) {
 | 
					  const getAllFiles = async () => {
 | 
				
			||||||
      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()
 | 
					 | 
				
			||||||
    try {
 | 
					    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: {
 | 
					        headers: {
 | 
				
			||||||
          'Content-Type': 'multipart/form-data',
 | 
					 | 
				
			||||||
          Authorization: `Bearer ${token}`,
 | 
					          Authorization: `Bearer ${token}`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      })
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (response.status === 200) {
 | 
					      if (response.status === 200) {
 | 
				
			||||||
        getAllFile()
 | 
					        notifications.show({
 | 
				
			||||||
        getTree()
 | 
					          title: 'Thành công',
 | 
				
			||||||
        setOtherFiles([])
 | 
					          message: 'Xóa file thành công',
 | 
				
			||||||
 | 
					          color: 'green',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await getAllFiles();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } 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(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    getTree()
 | 
					    getAllFiles()
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  const filterFiles = (files: FileData[], searchTerm: string) => {
 | 
				
			||||||
    getAllFile()
 | 
					    return files.filter(
 | 
				
			||||||
  }, [currentName])
 | 
					      (file) =>
 | 
				
			||||||
 | 
					        file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
				
			||||||
 | 
					        (file.description &&
 | 
				
			||||||
 | 
					          file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
      <div className={classes.title}>
 | 
					      <div className={classes.title}>
 | 
				
			||||||
        <h3>
 | 
					        <h3>
 | 
				
			||||||
          <Text>Admin/</Text>
 | 
					          <Text size="sm">Admin/</Text>
 | 
				
			||||||
          Profiles
 | 
					          Files Management
 | 
				
			||||||
        </h3>
 | 
					        </h3>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <Box ml={'lg'}>
 | 
					      <Box ml={'md'}>
 | 
				
			||||||
        <Tree
 | 
					        <Stack gap="xs">
 | 
				
			||||||
          classNames={classes}
 | 
					          {Object.entries(groupedFiles).map(([userName, files]) => (
 | 
				
			||||||
          selectOnClick
 | 
					            <Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
 | 
				
			||||||
          clearSelectionOnOutsideClick
 | 
					              <Group
 | 
				
			||||||
          data={treeData}
 | 
					                justify="space-between"
 | 
				
			||||||
          renderNode={(payload) => <Leaf {...payload} />}
 | 
					                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
 | 
					        <Modal
 | 
				
			||||||
          size={'lg'}
 | 
					          size="lg"
 | 
				
			||||||
          opened={openedProfile}
 | 
					          opened={openedProfile}
 | 
				
			||||||
          onClose={() => {
 | 
					          onClose={() => {
 | 
				
			||||||
            setOpenedProfile(false)
 | 
					            setOpenedProfile(false)
 | 
				
			||||||
 | 
					            setCurrentUser('')
 | 
				
			||||||
 | 
					            setSelectedFile(null)
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Box>
 | 
					          <Box>
 | 
				
			||||||
            <form onSubmit={handleSubmit}>
 | 
					            <FileUploadForm
 | 
				
			||||||
              <Stack>
 | 
					              data={groupedFiles[currentUser] || []}
 | 
				
			||||||
                <Box
 | 
					              handleSubmit={handleSubmit}
 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
					              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
				
			||||||
                >
 | 
					              removeFile={removeFile}
 | 
				
			||||||
                  <Text>CV</Text>
 | 
					              isLoading={isLoading}
 | 
				
			||||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
					              currentUser={currentUser}
 | 
				
			||||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
					            />
 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('cv')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label={'CV'}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CV"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    0
 | 
					 | 
				
			||||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.doc,.docx"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>CCCD</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('idCard')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="CCCD"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'none' : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CCCD"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bảng điểm</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('transcript')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bảng điểm"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'none' : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bảng điểm"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'flex'
 | 
					 | 
				
			||||||
                      : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bằng đại học</Text>
 | 
					 | 
				
			||||||
                  <a
 | 
					 | 
				
			||||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <Text>{`: ${
 | 
					 | 
				
			||||||
                      checkFileExist('universityDiploma')?.label
 | 
					 | 
				
			||||||
                    }`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button
 | 
					 | 
				
			||||||
                    variant="outline"
 | 
					 | 
				
			||||||
                    size="xs"
 | 
					 | 
				
			||||||
                    color="red"
 | 
					 | 
				
			||||||
                    ml={'sm'}
 | 
					 | 
				
			||||||
                    onClick={() => {
 | 
					 | 
				
			||||||
                      removeFile(checkFileExist('universityDiploma')?.value!)
 | 
					 | 
				
			||||||
                    }}
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bằng đại học"
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'none'
 | 
					 | 
				
			||||||
                      : 'block',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bằng đại học"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setUniversityDiploma(
 | 
					 | 
				
			||||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Text>Danh sách file khác:</Text>
 | 
					 | 
				
			||||||
                <Box>
 | 
					 | 
				
			||||||
                  {data
 | 
					 | 
				
			||||||
                    .find((f) => f.label === 'others')
 | 
					 | 
				
			||||||
                    ?.children?.map((c, index) => {
 | 
					 | 
				
			||||||
                      return (
 | 
					 | 
				
			||||||
                        <Box
 | 
					 | 
				
			||||||
                          key={index}
 | 
					 | 
				
			||||||
                          style={{
 | 
					 | 
				
			||||||
                            display: 'flex',
 | 
					 | 
				
			||||||
                            marginBottom: '5px',
 | 
					 | 
				
			||||||
                          }}
 | 
					 | 
				
			||||||
                        >
 | 
					 | 
				
			||||||
                          <a href={c?.value} target="_blank">
 | 
					 | 
				
			||||||
                            <Text>{`${c?.label}`}</Text>
 | 
					 | 
				
			||||||
                          </a>
 | 
					 | 
				
			||||||
                          <Button
 | 
					 | 
				
			||||||
                            variant="outline"
 | 
					 | 
				
			||||||
                            size="xs"
 | 
					 | 
				
			||||||
                            color="red"
 | 
					 | 
				
			||||||
                            ml={'sm'}
 | 
					 | 
				
			||||||
                            onClick={() => {
 | 
					 | 
				
			||||||
                              removeFile(c?.value!)
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                          >
 | 
					 | 
				
			||||||
                            Delete
 | 
					 | 
				
			||||||
                          </Button>
 | 
					 | 
				
			||||||
                        </Box>
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                {otherFiles.map((fileInput, index) => (
 | 
					 | 
				
			||||||
                  <Group key={index}>
 | 
					 | 
				
			||||||
                    <FileInput
 | 
					 | 
				
			||||||
                      placeholder="Chọn file"
 | 
					 | 
				
			||||||
                      onChange={(file) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(index, 'file', file!)
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      w={'30%'}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                    <TextInput
 | 
					 | 
				
			||||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
					 | 
				
			||||||
                      value={fileInput.type}
 | 
					 | 
				
			||||||
                      w={'65%'}
 | 
					 | 
				
			||||||
                      onChange={(e) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(
 | 
					 | 
				
			||||||
                          index,
 | 
					 | 
				
			||||||
                          'type',
 | 
					 | 
				
			||||||
                          e.currentTarget.value,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </Group>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
					 | 
				
			||||||
                  Thêm file khác
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Button type="submit" color="blue">
 | 
					 | 
				
			||||||
                  Save
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
              </Stack>
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Modal>
 | 
					        </Modal>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +1,16 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  deleteFile,
 | 
					  getFiles,
 | 
				
			||||||
  getAllFilesInProfiles,
 | 
					 | 
				
			||||||
  getProfilesData,
 | 
					  getProfilesData,
 | 
				
			||||||
  listUserTechnical,
 | 
					  listUserTechnical,
 | 
				
			||||||
  updateProfileFolder,
 | 
					 | 
				
			||||||
  updateProfilesData,
 | 
					  updateProfilesData,
 | 
				
			||||||
  updateUserTechnical,
 | 
					  updateUserTechnical,
 | 
				
			||||||
 | 
					  uploadFiles
 | 
				
			||||||
} from '@/api/Admin'
 | 
					} from '@/api/Admin'
 | 
				
			||||||
import { changePassword } from '@/api/Auth'
 | 
					import { changePassword } from '@/api/Auth'
 | 
				
			||||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
					import DataTableAll from '@/components/DataTable/DataTable'
 | 
				
			||||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
					import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
				
			||||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
					import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
				
			||||||
import { logout } from '@/rtk/dispatches/auth'
 | 
					import { logout } from '@/rtk/dispatches/auth'
 | 
				
			||||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
					 | 
				
			||||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
					import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
				
			||||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
					import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
				
			||||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
					import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
				
			||||||
| 
						 | 
					@ -22,16 +20,13 @@ import {
 | 
				
			||||||
  Avatar,
 | 
					  Avatar,
 | 
				
			||||||
  Box,
 | 
					  Box,
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
  FileInput,
 | 
					 | 
				
			||||||
  Flex,
 | 
					  Flex,
 | 
				
			||||||
  Group,
 | 
					 | 
				
			||||||
  Loader,
 | 
					  Loader,
 | 
				
			||||||
  Modal,
 | 
					  Modal,
 | 
				
			||||||
  PasswordInput,
 | 
					  PasswordInput,
 | 
				
			||||||
  Stack,
 | 
					 | 
				
			||||||
  Text,
 | 
					  Text,
 | 
				
			||||||
  TextInput,
 | 
					  TextInput,
 | 
				
			||||||
  Title,
 | 
					  Title
 | 
				
			||||||
} from '@mantine/core'
 | 
					} from '@mantine/core'
 | 
				
			||||||
import { notifications } from '@mantine/notifications'
 | 
					import { notifications } from '@mantine/notifications'
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -46,15 +41,26 @@ import moment from 'moment'
 | 
				
			||||||
import { useCallback, useEffect, useState } from 'react'
 | 
					import { useCallback, useEffect, useState } from 'react'
 | 
				
			||||||
import { useNavigate } from 'react-router-dom'
 | 
					import { useNavigate } from 'react-router-dom'
 | 
				
			||||||
import classes from './Profile.module.css'
 | 
					import classes from './Profile.module.css'
 | 
				
			||||||
 | 
					import FileUploadForm from './components/FileUploadForm'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const isCompactMenu = false
 | 
					const isCompactMenu = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TFileProfile = {
 | 
					// type TFileProfile = {
 | 
				
			||||||
  label: string
 | 
					//   label: string
 | 
				
			||||||
  type: string
 | 
					//   type: string
 | 
				
			||||||
  value: string
 | 
					//   value: string
 | 
				
			||||||
  children?: TFileProfile[]
 | 
					//   children?: TFileProfile[]
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileData {
 | 
				
			||||||
 | 
					  id: number;
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					  url: string;
 | 
				
			||||||
 | 
					  type: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  created_at: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Profile = () => {
 | 
					const Profile = () => {
 | 
				
			||||||
  const user = useAppSelector((state) => state.authentication.user)
 | 
					  const user = useAppSelector((state) => state.authentication.user)
 | 
				
			||||||
  const userData = getUser()
 | 
					  const userData = getUser()
 | 
				
			||||||
| 
						 | 
					@ -77,6 +83,11 @@ const Profile = () => {
 | 
				
			||||||
  const navigate = useNavigate()
 | 
					  const navigate = useNavigate()
 | 
				
			||||||
  const dispatch = useAppDispatch()
 | 
					  const dispatch = useAppDispatch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
				
			||||||
 | 
					  const [isLoading, setIsLoading] = useState(false)
 | 
				
			||||||
 | 
					  const [data, setData] = useState<FileData[]>([])
 | 
				
			||||||
 | 
					  const [openedProfile, setOpenedProfile] = useState(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateAvatar = async (file: File) => {
 | 
					  const updateAvatar = async (file: File) => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await postImage(updateProfilesData, file, 'post')
 | 
					      const res = await postImage(updateProfilesData, file, 'post')
 | 
				
			||||||
| 
						 | 
					@ -137,102 +148,113 @@ const Profile = () => {
 | 
				
			||||||
    return []
 | 
					    return []
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [cv, setCv] = useState<File>()
 | 
					  // const [cv, setCv] = useState<File>()
 | 
				
			||||||
  const [idCard, setIdCard] = useState<File>()
 | 
					  // const [idCard, setIdCard] = useState<File>()
 | 
				
			||||||
  const [transcript, setTranscript] = useState<File>()
 | 
					  // const [transcript, setTranscript] = useState<File>()
 | 
				
			||||||
  const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
					  // const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
				
			||||||
  const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
					  // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
				
			||||||
  const [data, setData] = useState<TFileProfile[]>([])
 | 
					  // const handleOtherFileChange = (
 | 
				
			||||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
					  //   index: number,
 | 
				
			||||||
  const handleOtherFileChange = (
 | 
					  //   field: string,
 | 
				
			||||||
    index: number,
 | 
					  //   value: File | string,
 | 
				
			||||||
    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()
 | 
					    e.preventDefault()
 | 
				
			||||||
 | 
					    setIsLoading(true)
 | 
				
			||||||
    const formData = new FormData()
 | 
					    const formData = new FormData()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Append each selected file to FormData
 | 
					    if (selectedFile) {
 | 
				
			||||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
					      formData.append('file', selectedFile)
 | 
				
			||||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
					      formData.append('name', fileName)
 | 
				
			||||||
        formData.append(
 | 
					      formData.append('description', description)
 | 
				
			||||||
          'files[]',
 | 
					      formData.append('user_name', user.user.name)
 | 
				
			||||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
					      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 () => {
 | 
					  const getAllFile = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const res = await get(getAllFilesInProfiles, {
 | 
					      const res = await get(getFiles)
 | 
				
			||||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      if (res.status === true) {
 | 
					      if (res.status === true) {
 | 
				
			||||||
        setData(res.data)
 | 
					        const userFiles = res.data[user.user.name] || [];
 | 
				
			||||||
 | 
					        setData(userFiles);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(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 {
 | 
					    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) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.log(error)
 | 
					      console.log(error);
 | 
				
			||||||
 | 
					      notifications.show({
 | 
				
			||||||
 | 
					        title: 'Lỗi',
 | 
				
			||||||
 | 
					        message: 'Không thể xóa file',
 | 
				
			||||||
 | 
					        color: 'red',
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const fetchData = async () => {
 | 
					    const fetchData = async () => {
 | 
				
			||||||
      const result = await getListProfilesData()
 | 
					      const result = await getListProfilesData()
 | 
				
			||||||
      setDataProfile(result ?? [])
 | 
					      setDataProfile(result ?? [])
 | 
				
			||||||
 | 
					      await getAllFile()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    fetchData()
 | 
					    fetchData()
 | 
				
			||||||
    getAllFile()
 | 
					 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleChangePassword = async () => {
 | 
					  const handleChangePassword = async () => {
 | 
				
			||||||
| 
						 | 
					@ -306,23 +328,19 @@ const Profile = () => {
 | 
				
			||||||
    dispatch(logout(navigate))
 | 
					    dispatch(logout(navigate))
 | 
				
			||||||
  }, [dispatch, navigate])
 | 
					  }, [dispatch, navigate])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
					  // const handleChangeFileName = (e: File, newName: string): File => {
 | 
				
			||||||
    const originalFile = e // Get the original file
 | 
					  //   const originalFile = e;
 | 
				
			||||||
    const extend = originalFile.name.split('.')[1]
 | 
					  //   const extend = originalFile.name.split('.')[1];
 | 
				
			||||||
    if (originalFile) {
 | 
					  //   const newFileName = `${newName}.${extend}`;
 | 
				
			||||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
					  //   return new File([originalFile], newFileName, {
 | 
				
			||||||
      const newFile = new File([originalFile], newFileName, {
 | 
					  //     type: originalFile.type,
 | 
				
			||||||
        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.name.includes(nameField));
 | 
				
			||||||
  }
 | 
					  //   return file;
 | 
				
			||||||
 | 
					  // };
 | 
				
			||||||
  const checkFileExist = (nameField: string) => {
 | 
					 | 
				
			||||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
					 | 
				
			||||||
    return file
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div>
 | 
					    <div>
 | 
				
			||||||
| 
						 | 
					@ -578,162 +596,14 @@ const Profile = () => {
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Box>
 | 
					          <Box>
 | 
				
			||||||
            <form onSubmit={handleSubmit}>
 | 
					            <FileUploadForm
 | 
				
			||||||
              <Stack>
 | 
					              data={data}
 | 
				
			||||||
                <Box
 | 
					              handleSubmit={handleSubmit}
 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
					              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
				
			||||||
                >
 | 
					              removeFile={removeFile}
 | 
				
			||||||
                  <Text>CV</Text>
 | 
					              isLoading={isLoading}
 | 
				
			||||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
					              currentUser={user.user.name}
 | 
				
			||||||
                    <Text>{`: ${checkFileExist('cv')?.label}`}</Text>
 | 
					            />
 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('cv')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label={'CV'}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CV"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('cv') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    0
 | 
					 | 
				
			||||||
                    setCv(handleChangeFileName(e!, 'cv'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.doc,.docx"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('idCard') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>CCCD</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('idCard')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('idCard')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('idCard')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="CCCD"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('idCard') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên CCCD"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setIdCard(handleChangeFileName(e!, 'idCard'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".jpg,.jpeg,.png,.pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('transcript') ? 'flex' : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bảng điểm</Text>
 | 
					 | 
				
			||||||
                  <a href={checkFileExist('transcript')?.value} target="_blank">
 | 
					 | 
				
			||||||
                    <Text>{`: ${checkFileExist('transcript')?.label}`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('transcript')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bảng điểm"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('transcript') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bảng điểm"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setTranscript(handleChangeFileName(e!, 'transcript'))
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Box
 | 
					 | 
				
			||||||
                  style={{
 | 
					 | 
				
			||||||
                    display: checkFileExist('universityDiploma')
 | 
					 | 
				
			||||||
                      ? 'flex'
 | 
					 | 
				
			||||||
                      : 'none',
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <Text>Bằng đại học</Text>
 | 
					 | 
				
			||||||
                  <a
 | 
					 | 
				
			||||||
                    href={checkFileExist('universityDiploma')?.value}
 | 
					 | 
				
			||||||
                    target="_blank"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <Text>{`: ${
 | 
					 | 
				
			||||||
                      checkFileExist('universityDiploma')?.label
 | 
					 | 
				
			||||||
                    }`}</Text>
 | 
					 | 
				
			||||||
                  </a>
 | 
					 | 
				
			||||||
                  <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(checkFileExist('universityDiploma')?.value!)}}>
 | 
					 | 
				
			||||||
                    Delete
 | 
					 | 
				
			||||||
                  </Button>
 | 
					 | 
				
			||||||
                </Box>
 | 
					 | 
				
			||||||
                <FileInput
 | 
					 | 
				
			||||||
                  label="Bằng đại học"
 | 
					 | 
				
			||||||
                  style={{ display: checkFileExist('universityDiploma') ? 'none' : 'block' }}
 | 
					 | 
				
			||||||
                  placeholder="Tải lên bằng đại học"
 | 
					 | 
				
			||||||
                  onChange={(e) => {
 | 
					 | 
				
			||||||
                    setUniversityDiploma(
 | 
					 | 
				
			||||||
                      handleChangeFileName(e!, 'universityDiploma'),
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                  }}
 | 
					 | 
				
			||||||
                  accept=".pdf,.jpg,.jpeg,.png"
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Text>Danh sách file khác:</Text>
 | 
					 | 
				
			||||||
                  <Box>
 | 
					 | 
				
			||||||
                    {data.find((f)=>f.label === 'others')?.children?.map((c, index)=>{
 | 
					 | 
				
			||||||
                      return <Box key={index}
 | 
					 | 
				
			||||||
                      style={{
 | 
					 | 
				
			||||||
                        display: 'flex',
 | 
					 | 
				
			||||||
                        marginBottom:'5px'
 | 
					 | 
				
			||||||
                      }}
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      <a
 | 
					 | 
				
			||||||
                        href={c?.value}
 | 
					 | 
				
			||||||
                        target="_blank"
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                        <Text>{`${
 | 
					 | 
				
			||||||
                          c?.label
 | 
					 | 
				
			||||||
                        }`}</Text>
 | 
					 | 
				
			||||||
                      </a>
 | 
					 | 
				
			||||||
                      <Button variant="outline" size="xs" color="red" ml={'sm'} onClick={()=>{removeFile(c?.value!)}}>
 | 
					 | 
				
			||||||
                        Delete
 | 
					 | 
				
			||||||
                      </Button>
 | 
					 | 
				
			||||||
                    </Box>
 | 
					 | 
				
			||||||
                    })}
 | 
					 | 
				
			||||||
                  </Box>
 | 
					 | 
				
			||||||
                {otherFiles.map((fileInput, index) => (
 | 
					 | 
				
			||||||
                  <Group key={index}>
 | 
					 | 
				
			||||||
                    <FileInput
 | 
					 | 
				
			||||||
                      placeholder="Chọn file"
 | 
					 | 
				
			||||||
                      onChange={(file) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(index, 'file', file!)
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                      w={'30%'}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                    <TextInput
 | 
					 | 
				
			||||||
                      placeholder="Nhập tên file (không cần phần mở rộng)"
 | 
					 | 
				
			||||||
                      value={fileInput.type}
 | 
					 | 
				
			||||||
                      w={'65%'}
 | 
					 | 
				
			||||||
                      onChange={(e) =>
 | 
					 | 
				
			||||||
                        handleOtherFileChange(
 | 
					 | 
				
			||||||
                          index,
 | 
					 | 
				
			||||||
                          'type',
 | 
					 | 
				
			||||||
                          e.currentTarget.value,
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                      }
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                  </Group>
 | 
					 | 
				
			||||||
                ))}
 | 
					 | 
				
			||||||
                <Button type="button" onClick={addOtherFileInput}>
 | 
					 | 
				
			||||||
                  Thêm file khác
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                <Button type="submit" color="blue">
 | 
					 | 
				
			||||||
                  Save
 | 
					 | 
				
			||||||
                </Button>
 | 
					 | 
				
			||||||
              </Stack>
 | 
					 | 
				
			||||||
            </form>
 | 
					 | 
				
			||||||
          </Box>
 | 
					          </Box>
 | 
				
			||||||
        </Modal>
 | 
					        </Modal>
 | 
				
			||||||
      </Box>
 | 
					      </Box>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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); }
 | 
				
			||||||
 | 
					} 
 | 
				
			||||||
| 
						 | 
					@ -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<boolean | void>;
 | 
				
			||||||
 | 
					  handleFileChange: (file: File | null) => void;
 | 
				
			||||||
 | 
					  removeFile: (id: number) => Promise<void>;
 | 
				
			||||||
 | 
					  isLoading: boolean;
 | 
				
			||||||
 | 
					  currentUser: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FileUploadForm = ({
 | 
				
			||||||
 | 
					  data,
 | 
				
			||||||
 | 
					  handleSubmit,
 | 
				
			||||||
 | 
					  handleFileChange,
 | 
				
			||||||
 | 
					  removeFile,
 | 
				
			||||||
 | 
					  isLoading,
 | 
				
			||||||
 | 
					  currentUser,
 | 
				
			||||||
 | 
					}: FileUploadFormProps) => {
 | 
				
			||||||
 | 
					  const [selectedFile, setSelectedFile] = useState<File | null>(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 <IconFileTypeDocx size={16} />
 | 
				
			||||||
 | 
					      case 'image':
 | 
				
			||||||
 | 
					        return <IconPhoto size={16} />
 | 
				
			||||||
 | 
					      case 'spreadsheet':
 | 
				
			||||||
 | 
					        return <IconFileTypeXls size={16} />
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return <IconFileTypePdf size={16} />
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const filteredFiles = data.filter(
 | 
				
			||||||
 | 
					    (file) =>
 | 
				
			||||||
 | 
					      file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
				
			||||||
 | 
					      (file.description &&
 | 
				
			||||||
 | 
					        file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {isLoading && (
 | 
				
			||||||
 | 
					        <div className={classes.loadingOverlay}>
 | 
				
			||||||
 | 
					          <div className={classes.loadingSpinner} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <form onSubmit={handleFormSubmit}>
 | 
				
			||||||
 | 
					        <Box>
 | 
				
			||||||
 | 
					          <Text className={classes.sectionTitle}>Tài liệu</Text>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Box className={classes.fileInputGroup}>
 | 
				
			||||||
 | 
					            <FileInput
 | 
				
			||||||
 | 
					              label="Chọn file"
 | 
				
			||||||
 | 
					              placeholder="Chọn file để tải lên"
 | 
				
			||||||
 | 
					              accept="image/png,image/jpeg,image/jpg,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.spreadsheet"
 | 
				
			||||||
 | 
					              onChange={handleFileSelect}
 | 
				
			||||||
 | 
					              value={selectedFile}
 | 
				
			||||||
 | 
					              className={classes.fileInput}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <TextInput
 | 
				
			||||||
 | 
					              label="Tên file"
 | 
				
			||||||
 | 
					              placeholder="Nhập tên file"
 | 
				
			||||||
 | 
					              value={fileName}
 | 
				
			||||||
 | 
					              onChange={(e) => setFileName(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.fileNameInput}
 | 
				
			||||||
 | 
					              required
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Textarea
 | 
				
			||||||
 | 
					              label="Mô tả"
 | 
				
			||||||
 | 
					              placeholder="Nhập mô tả cho file"
 | 
				
			||||||
 | 
					              value={description}
 | 
				
			||||||
 | 
					              onChange={(e) => setDescription(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.descriptionInput}
 | 
				
			||||||
 | 
					              minRows={3}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              type="submit"
 | 
				
			||||||
 | 
					              color="blue"
 | 
				
			||||||
 | 
					              className={classes.saveButton}
 | 
				
			||||||
 | 
					              disabled={isLoading || isUploading || !selectedFile || !fileName}
 | 
				
			||||||
 | 
					              loading={isLoading || isUploading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              {isLoading || isUploading ? 'Đang xử lý...' : 'Lưu thay đổi'}
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <Box className={classes.fileListContainer}>
 | 
				
			||||||
 | 
					            <TextInput
 | 
				
			||||||
 | 
					              placeholder="Tìm kiếm theo tên hoặc mô tả..."
 | 
				
			||||||
 | 
					              leftSection={<IconSearch size={14} />}
 | 
				
			||||||
 | 
					              value={searchTerm}
 | 
				
			||||||
 | 
					              onChange={(e) => setSearchTerm(e.target.value)}
 | 
				
			||||||
 | 
					              className={classes.searchInput}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <Stack className={classes.fileList} gap="xs">
 | 
				
			||||||
 | 
					              {filteredFiles.map((file) => (
 | 
				
			||||||
 | 
					                <Card
 | 
				
			||||||
 | 
					                  key={file.id}
 | 
				
			||||||
 | 
					                  shadow="xs"
 | 
				
			||||||
 | 
					                  padding="xs"
 | 
				
			||||||
 | 
					                  radius="sm"
 | 
				
			||||||
 | 
					                  withBorder
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  <Group justify="space-between" gap="xs" wrap="nowrap">
 | 
				
			||||||
 | 
					                    <Group gap="xs" className={classes.cardContent}>
 | 
				
			||||||
 | 
					                      {getFileIcon(file.type)}
 | 
				
			||||||
 | 
					                      <Box style={{ minWidth: 0 }}>
 | 
				
			||||||
 | 
					                        <Text size="xs" fw={500} className={classes.cardTitle}>
 | 
				
			||||||
 | 
					                          {file.name}
 | 
				
			||||||
 | 
					                        </Text>
 | 
				
			||||||
 | 
					                        {file.description && (
 | 
				
			||||||
 | 
					                          <Text
 | 
				
			||||||
 | 
					                            size="xs"
 | 
				
			||||||
 | 
					                            c="dimmed"
 | 
				
			||||||
 | 
					                            className={classes.cardDescription}
 | 
				
			||||||
 | 
					                          >
 | 
				
			||||||
 | 
					                            {file.description}
 | 
				
			||||||
 | 
					                          </Text>
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                        <Text size="xs" c="dimmed">
 | 
				
			||||||
 | 
					                          Uploaded:{' '}
 | 
				
			||||||
 | 
					                          {new Date(file.created_at).toLocaleDateString()}
 | 
				
			||||||
 | 
					                        </Text>
 | 
				
			||||||
 | 
					                      </Box>
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                    <Group gap="xs" wrap="nowrap">
 | 
				
			||||||
 | 
					                      <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} />
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                      <Button
 | 
				
			||||||
 | 
					                        size="xs"
 | 
				
			||||||
 | 
					                        variant="light"
 | 
				
			||||||
 | 
					                        color="red"
 | 
				
			||||||
 | 
					                        onClick={() => removeFile(file.id)}
 | 
				
			||||||
 | 
					                      >
 | 
				
			||||||
 | 
					                        <Group gap={2}>
 | 
				
			||||||
 | 
					                          <IconTrash size={12} />
 | 
				
			||||||
 | 
					                        </Group>
 | 
				
			||||||
 | 
					                      </Button>
 | 
				
			||||||
 | 
					                    </Group>
 | 
				
			||||||
 | 
					                  </Group>
 | 
				
			||||||
 | 
					                </Card>
 | 
				
			||||||
 | 
					              ))}
 | 
				
			||||||
 | 
					            </Stack>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					        </Box>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FileUploadForm
 | 
				
			||||||
		Loading…
	
		Reference in New Issue