truong-leave-day #116
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -10,9 +10,14 @@ use App\Traits\HasFilterRequest;
 | 
			
		|||
use App\Traits\HasOrderByRequest;
 | 
			
		||||
use App\Traits\HasSearchRequest;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Mail;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Illuminate\Support\Facades\Log;
 | 
			
		||||
use Modules\Admin\app\Models\Admin;
 | 
			
		||||
use Modules\Admin\app\Models\Sprint;
 | 
			
		||||
use Modules\Admin\app\Models\UserCriteria;
 | 
			
		||||
use App\Models\Files;
 | 
			
		||||
use App\DataTransferObjects\FileData;
 | 
			
		||||
 | 
			
		||||
class ProfileController extends Controller
 | 
			
		||||
{
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +33,7 @@ class ProfileController extends Controller
 | 
			
		|||
        $this->jiraService = $jiraService;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public function getProfilesData(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $user = auth('admins')->user();
 | 
			
		||||
| 
						 | 
				
			
			@ -125,7 +131,7 @@ class ProfileController extends Controller
 | 
			
		|||
        $rootFolder = rtrim($rootFolder, '/') . '/';
 | 
			
		||||
 | 
			
		||||
        // Get all files and directories in the specified root folder
 | 
			
		||||
        $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder: 'image'.$rootFolder);
 | 
			
		||||
        $fileList = $this->getDirectoryTree(public_path($rootFolder), env('APP_ENV') === 'local' ? $rootFolder : 'image' . $rootFolder);
 | 
			
		||||
 | 
			
		||||
        return response()->json(['data' => $fileList, 'status' => true]);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -185,6 +191,10 @@ class ProfileController extends Controller
 | 
			
		|||
        if (!Storage::disk('public')->exists($othersDirectory)) {
 | 
			
		||||
            Storage::disk('public')->makeDirectory($othersDirectory);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $adminEmails = Admin::where('permission', 'like', '%admin%')->pluck('email')->toArray();
 | 
			
		||||
        $currentUser = auth('admins')->user();
 | 
			
		||||
 | 
			
		||||
        if ($request->hasFile('files')) {
 | 
			
		||||
            foreach ($request->file('files') as $file) {
 | 
			
		||||
                // Store the file and get its path
 | 
			
		||||
| 
						 | 
				
			
			@ -197,6 +207,32 @@ class ProfileController extends Controller
 | 
			
		|||
                    $path = $file->storeAs($baseDirectory, $originalFilename, 'public');
 | 
			
		||||
                }
 | 
			
		||||
                $uploadedFiles[] = $path;
 | 
			
		||||
 | 
			
		||||
                // Tạo URL đầy đủ cho file
 | 
			
		||||
                $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') 
 | 
			
		||||
                    ? env('APP_URL') . '/image/' . str_replace('/storage/', '', Storage::url($path)) 
 | 
			
		||||
                    : env('APP_URL') . str_replace('/storage/', '', Storage::url($path));
 | 
			
		||||
 | 
			
		||||
                // // Gửi email thông báo cho admin
 | 
			
		||||
                // foreach ($adminEmails as $adminEmail) {
 | 
			
		||||
                //     $admin = Admin::where('email', $adminEmail)->first();
 | 
			
		||||
                //     if ($admin) {
 | 
			
		||||
                //         $this->sendFileUploadNotification(
 | 
			
		||||
                //             $admin,
 | 
			
		||||
                //             "File {$originalFilename} đã được tải lên bởi {$currentUser->name}",
 | 
			
		||||
                //             $fileUrl,
 | 
			
		||||
                //             "[APAC Tech] {$currentUser->name} - Đã tải lên file mới"
 | 
			
		||||
                //         );
 | 
			
		||||
                //     }
 | 
			
		||||
                // }
 | 
			
		||||
 | 
			
		||||
                // // Gửi email xác nhận cho người tải lên
 | 
			
		||||
                // $this->sendFileUploadNotification(
 | 
			
		||||
                //     $currentUser,
 | 
			
		||||
                //     "Bạn đã tải lên file {$originalFilename} thành công",
 | 
			
		||||
                //     $fileUrl,
 | 
			
		||||
                //     "[APAC Tech] {$currentUser->name} - Tải file thành công"
 | 
			
		||||
                // );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -237,4 +273,195 @@ class ProfileController extends Controller
 | 
			
		|||
            'message' => 'File not found',
 | 
			
		||||
        ], 404);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function sendFileUploadNotification($user, $description, $url, $subject, $note)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            // Gửi email bất đồng bộ không cần job
 | 
			
		||||
            dispatch(function() use ($user, $description, $url, $subject, $note) {
 | 
			
		||||
                Mail::send('emails.file_upload_notification', [
 | 
			
		||||
                    'user' => $user,
 | 
			
		||||
                    'description' => $description,
 | 
			
		||||
                    'url' => $url, 
 | 
			
		||||
                    'note' => $note
 | 
			
		||||
                ], function ($message) use ($user, $subject) {
 | 
			
		||||
                    $message->to($user->email)
 | 
			
		||||
                        ->subject($subject);
 | 
			
		||||
                });
 | 
			
		||||
            })->afterResponse();
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            Log::error('Error dispatching file upload notification email: ' . $e->getMessage());
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function uploadFiles(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $request->validate([
 | 
			
		||||
                'file' => 'required|file|mimes:jpg,jpeg,png,pdf,doc,docx,xls,xlsx,csv|max:5120',
 | 
			
		||||
                'name' => 'required|string|max:255', 
 | 
			
		||||
                'description' => 'nullable|string',
 | 
			
		||||
                'user_name' => 'required|string|max:255'
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $file = $request->file('file');
 | 
			
		||||
            $user = auth('admins')->user();
 | 
			
		||||
            
 | 
			
		||||
            // Tạo thư mục cho user nếu chưa tồn tại
 | 
			
		||||
            $userFolder = 'files/' . $request->user_name;
 | 
			
		||||
            if (!Storage::disk('public')->exists($userFolder)) {
 | 
			
		||||
                Storage::disk('public')->makeDirectory($userFolder);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            $path = $file->store($userFolder, 'public');
 | 
			
		||||
            
 | 
			
		||||
            $fileRecord = Files::create([
 | 
			
		||||
                'name' => $request->name,
 | 
			
		||||
                'url' => $path,
 | 
			
		||||
                'type' => $this->getFileType($file->getClientOriginalName()),
 | 
			
		||||
                'description' => $request->description,
 | 
			
		||||
                'user_id' => Admin::where('name', $request->user_name)->first()->id
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            $currentUser = Admin::where('name', $request->user_name)->first();
 | 
			
		||||
            // Gửi email thông báo cho người upload
 | 
			
		||||
            $fileUrl = (env('APP_ENV') === 'prod' || env('APP_ENV') === 'production') 
 | 
			
		||||
                ? env('APP_URL') . '/image' . Storage::url($path) 
 | 
			
		||||
                : env('APP_URL') . Storage::url($path);
 | 
			
		||||
            $this->sendFileUploadNotification(
 | 
			
		||||
                $user,
 | 
			
		||||
                'Bạn đã tải lên file "' . $request->name . '" thành công',
 | 
			
		||||
                $fileUrl,
 | 
			
		||||
                "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
 | 
			
		||||
                $request->description ?? 'No description'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            // Gửi email thông báo cho tất cả admin khác
 | 
			
		||||
            $otherAdmins = Admin::where('permission', 'like', '%admin%')->get();
 | 
			
		||||
            foreach ($otherAdmins as $admin) {
 | 
			
		||||
                $this->sendFileUploadNotification(
 | 
			
		||||
                    $admin,
 | 
			
		||||
                    'File "' . $request->name . '" đã được tải lên bởi ' . $user->name,
 | 
			
		||||
                    $fileUrl,
 | 
			
		||||
                    "[APAC Tech] {$currentUser->name} - Đã tải lên file mới",
 | 
			
		||||
                    $request->description ?? 'No description'
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => true,
 | 
			
		||||
                'message' => 'File uploaded successfully',
 | 
			
		||||
                'data' => [
 | 
			
		||||
                    'id' => $fileRecord->id,
 | 
			
		||||
                    'name' => $fileRecord->name,
 | 
			
		||||
                    'url' => Storage::url($path),
 | 
			
		||||
                    'type' => $fileRecord->type,
 | 
			
		||||
                    'description' => $fileRecord->description
 | 
			
		||||
                ]
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => false,
 | 
			
		||||
                'message' => $e->getMessage()
 | 
			
		||||
            ], 500);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getFiles()
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $files = Files::with('user')->get()
 | 
			
		||||
                ->map(function($file) {
 | 
			
		||||
                    return [
 | 
			
		||||
                        'id' => $file->id,
 | 
			
		||||
                        'name' => $file->name,
 | 
			
		||||
                        'url' => Storage::url($file->url),
 | 
			
		||||
                        'type' => $file->type,
 | 
			
		||||
                        'description' => $file->description,
 | 
			
		||||
                        'created_at' => $file->created_at,
 | 
			
		||||
                        'user_id' => $file->user_id,
 | 
			
		||||
                        'user_name' => $file->user->name
 | 
			
		||||
                    ];
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            // Gom nhóm files theo tên user
 | 
			
		||||
            $groupedFiles = $files->groupBy('user_name')
 | 
			
		||||
                ->map(function($files) {
 | 
			
		||||
                    return $files->map(function(array $file) {
 | 
			
		||||
                        return (object)[
 | 
			
		||||
                            'id' => $file['id'],
 | 
			
		||||
                            'name' => $file['name'],
 | 
			
		||||
                            'url' => $file['url'],
 | 
			
		||||
                            'type' => $file['type'],
 | 
			
		||||
                            'description' => $file['description'],
 | 
			
		||||
                            'created_at' => $file['created_at'],
 | 
			
		||||
                            'user_id' => $file['user_id']
 | 
			
		||||
                        ];
 | 
			
		||||
                    });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => true,
 | 
			
		||||
                'data' => $groupedFiles
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => false,
 | 
			
		||||
                'message' => $e->getMessage()
 | 
			
		||||
            ], 500);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function deleteFile($id)
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $file = Files::findOrFail($id);
 | 
			
		||||
            $user = auth('admins')->user();
 | 
			
		||||
            
 | 
			
		||||
            if ($file->user_id !== $user->id) {
 | 
			
		||||
                return response()->json([
 | 
			
		||||
                    'status' => false,
 | 
			
		||||
                    'message' => 'Unauthorized'
 | 
			
		||||
                ], 403);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Storage::disk('public')->delete($file->url);
 | 
			
		||||
            $file->delete();
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => true,
 | 
			
		||||
                'message' => 'File deleted successfully'
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => false,
 | 
			
		||||
                'message' => $e->getMessage()
 | 
			
		||||
            ], 500);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getFileType($filename)
 | 
			
		||||
    {
 | 
			
		||||
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
 | 
			
		||||
        
 | 
			
		||||
        $typeMap = [
 | 
			
		||||
            'pdf' => 'document',
 | 
			
		||||
            'doc' => 'document',
 | 
			
		||||
            'docx' => 'document',
 | 
			
		||||
            'jpg' => 'image',
 | 
			
		||||
            'jpeg' => 'image',
 | 
			
		||||
            'png' => 'image',
 | 
			
		||||
            'xls' => 'spreadsheet',
 | 
			
		||||
            'xlsx' => 'spreadsheet',
 | 
			
		||||
            'csv' => 'spreadsheet'
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return $typeMap[$extension] ?? 'other';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,6 +174,9 @@ Route::middleware('api')
 | 
			
		|||
                Route::get('/all-files', [ProfileController::class, 'listFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/update-profile', [ProfileController::class, 'updateProfile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::get('/delete-profile-file', [ProfileController::class, 'removeFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::get('/files', [ProfileController::class, 'getFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/upload-files', [ProfileController::class, 'uploadFiles'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::delete('/files/{id}', [ProfileController::class, 'deleteFile'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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',
 | 
			
		||||
        //     '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
 | 
			
		||||
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,195 +1,95 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { getFiles, uploadFiles } from '@/api/Admin'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Card,
 | 
			
		||||
  Collapse,
 | 
			
		||||
  Group,
 | 
			
		||||
  Modal,
 | 
			
		||||
  RenderTreeNodePayload,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Tree,
 | 
			
		||||
  Title,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconChevronDown,
 | 
			
		||||
  IconDownload,
 | 
			
		||||
  IconFileTypeDocx,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFolder,
 | 
			
		||||
  IconFolderOpen,
 | 
			
		||||
  IconFolderX,
 | 
			
		||||
  IconListCheck,
 | 
			
		||||
  IconPhoto,
 | 
			
		||||
  IconSearch,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import FileUploadForm from '../Profile/components/FileUploadForm'
 | 
			
		||||
import classes from './AllProfiles.module.css'
 | 
			
		||||
 | 
			
		||||
interface FileIconProps {
 | 
			
		||||
interface FileData {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  isFolder: boolean
 | 
			
		||||
  expanded: boolean
 | 
			
		||||
  url: string
 | 
			
		||||
  type: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  created_at: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
interface GroupedFiles {
 | 
			
		||||
  [key: string]: FileData[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AllProfiles = () => {
 | 
			
		||||
  const [treeData, setTreeData] = useState([])
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  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 [groupedFiles, setGroupedFiles] = useState<GroupedFiles>({})
 | 
			
		||||
  const [currentUser, setCurrentUser] = useState<string>('')
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  function FileIcon({ name, isFolder, expanded }: FileIconProps) {
 | 
			
		||||
    if (name.endsWith('.pdf')) {
 | 
			
		||||
      return <IconFileTypePdf size={14} />
 | 
			
		||||
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
  const [expandedFolders, setExpandedFolders] = useState<{
 | 
			
		||||
    [key: string]: boolean
 | 
			
		||||
  }>({})
 | 
			
		||||
  const [searchTerms, setSearchTerms] = useState<{ [key: string]: string }>({})
 | 
			
		||||
 | 
			
		||||
  const toggleFolder = (userName: string) => {
 | 
			
		||||
    setExpandedFolders((prev) => ({
 | 
			
		||||
      ...prev,
 | 
			
		||||
      [userName]: !prev[userName],
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
			
		||||
      return <IconFileTypeDocx size={14} />
 | 
			
		||||
  const getFileIcon = (type: string) => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'document':
 | 
			
		||||
        return <IconFileTypeDocx size={16} />
 | 
			
		||||
      case 'image':
 | 
			
		||||
        return <IconPhoto size={16} />
 | 
			
		||||
      default:
 | 
			
		||||
        return <IconFileTypePdf size={16} />
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    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({
 | 
			
		||||
    node,
 | 
			
		||||
    expanded,
 | 
			
		||||
    hasChildren,
 | 
			
		||||
    elementProps,
 | 
			
		||||
  }: RenderTreeNodePayload) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Group gap={5} {...elementProps}>
 | 
			
		||||
        {!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 = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  const handleSubmit = async (
 | 
			
		||||
    e: React.FormEvent,
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
    currentUser: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
      formData.append('file', selectedFile)
 | 
			
		||||
      formData.append('name', fileName)
 | 
			
		||||
      formData.append('description', description)
 | 
			
		||||
      formData.append('user_name', currentUser)
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    formData.append('name', currentName)
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
      try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
| 
						 | 
				
			
			@ -197,302 +97,234 @@ const AllProfiles = () => {
 | 
			
		|||
        })
 | 
			
		||||
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        getTree()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
          await getAllFiles()
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
        throw error
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
  const getAllFiles = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + currentName,
 | 
			
		||||
      })
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
        setGroupedFiles(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    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)
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFiles();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getTree()
 | 
			
		||||
    getAllFiles()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [currentName])
 | 
			
		||||
  const filterFiles = (files: FileData[], searchTerm: string) => {
 | 
			
		||||
    return files.filter(
 | 
			
		||||
      (file) =>
 | 
			
		||||
        file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
        (file.description &&
 | 
			
		||||
          file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          <Text>Admin/</Text>
 | 
			
		||||
          Profiles
 | 
			
		||||
          <Text size="sm">Admin/</Text>
 | 
			
		||||
          Files Management
 | 
			
		||||
        </h3>
 | 
			
		||||
      </div>
 | 
			
		||||
      <Box ml={'lg'}>
 | 
			
		||||
        <Tree
 | 
			
		||||
          classNames={classes}
 | 
			
		||||
          selectOnClick
 | 
			
		||||
          clearSelectionOnOutsideClick
 | 
			
		||||
          data={treeData}
 | 
			
		||||
          renderNode={(payload) => <Leaf {...payload} />}
 | 
			
		||||
      <Box ml={'md'}>
 | 
			
		||||
        <Stack gap="xs">
 | 
			
		||||
          {Object.entries(groupedFiles).map(([userName, files]) => (
 | 
			
		||||
            <Card key={userName} shadow="xs" radius="sm" withBorder p="xs">
 | 
			
		||||
              <Group
 | 
			
		||||
                justify="space-between"
 | 
			
		||||
                mb="xs"
 | 
			
		||||
                gap="xs"
 | 
			
		||||
                onClick={() => toggleFolder(userName)}
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
              >
 | 
			
		||||
                <Group gap="xs">
 | 
			
		||||
                  <IconFolder size={18} color="var(--mantine-color-yellow-9)" />
 | 
			
		||||
                  <Title order={5}>{userName}</Title>
 | 
			
		||||
                </Group>
 | 
			
		||||
                <Group gap="xs">
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    variant="subtle"
 | 
			
		||||
                    color="gray"
 | 
			
		||||
                    onClick={() => toggleFolder(userName)}
 | 
			
		||||
                    leftSection={
 | 
			
		||||
                      <IconChevronDown
 | 
			
		||||
                        size={14}
 | 
			
		||||
                        style={{
 | 
			
		||||
                          transform: expandedFolders[userName]
 | 
			
		||||
                            ? 'rotate(180deg)'
 | 
			
		||||
                            : 'none',
 | 
			
		||||
                          transition: 'transform 0.2s',
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    {expandedFolders[userName] ? 'Collapse' : 'Expand'}
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    variant="light"
 | 
			
		||||
                    color="blue"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setCurrentUser(userName)
 | 
			
		||||
                      setOpenedProfile(true)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Group gap={2}>
 | 
			
		||||
                      <IconListCheck size={14} />
 | 
			
		||||
                      <Text size="xs">Upload Files</Text>
 | 
			
		||||
                    </Group>
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Group>
 | 
			
		||||
              </Group>
 | 
			
		||||
              <Collapse in={expandedFolders[userName]}>
 | 
			
		||||
                <Stack gap="xs">
 | 
			
		||||
                  <TextInput
 | 
			
		||||
                    placeholder="Search files by name or description..."
 | 
			
		||||
                    size="xs"
 | 
			
		||||
                    leftSection={<IconSearch size={14} />}
 | 
			
		||||
                    value={searchTerms[userName] || ''}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                      setSearchTerms((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [userName]: e.target.value,
 | 
			
		||||
                      }))
 | 
			
		||||
                    }
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                  />
 | 
			
		||||
                  {filterFiles(files, searchTerms[userName] || '')
 | 
			
		||||
                    .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
 | 
			
		||||
                    .map((file: FileData) => (
 | 
			
		||||
                      <Card
 | 
			
		||||
                        key={file.id}
 | 
			
		||||
                        shadow="xs"
 | 
			
		||||
                        padding="xs"
 | 
			
		||||
                        radius="sm"
 | 
			
		||||
                        withBorder
 | 
			
		||||
                      >
 | 
			
		||||
                        <Group justify="space-between" gap="xs">
 | 
			
		||||
                          <Group gap="xs">
 | 
			
		||||
                            {getFileIcon(file.type)}
 | 
			
		||||
                            <Box>
 | 
			
		||||
                              <Text size="xs" fw={500}>
 | 
			
		||||
                                {file.name}
 | 
			
		||||
                              </Text>
 | 
			
		||||
                              {file.description && (
 | 
			
		||||
                                <Text size="xs" c="dimmed">
 | 
			
		||||
                                  {file.description}
 | 
			
		||||
                                </Text>
 | 
			
		||||
                              )}
 | 
			
		||||
                              <Text size="xs" c="dimmed">
 | 
			
		||||
                                Uploaded:{' '}
 | 
			
		||||
                                {new Date(file.created_at).toLocaleDateString()}
 | 
			
		||||
                              </Text>
 | 
			
		||||
                            </Box>
 | 
			
		||||
                          </Group>
 | 
			
		||||
                          <Group gap="xs">
 | 
			
		||||
                            <Button
 | 
			
		||||
                              size="xs"
 | 
			
		||||
                              variant="light" 
 | 
			
		||||
                              color="blue"
 | 
			
		||||
                              component='a'
 | 
			
		||||
                              href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
                                import.meta.env.VITE_BACKEND_URL?.includes(
 | 
			
		||||
                                  'localhost',
 | 
			
		||||
                                )
 | 
			
		||||
                                  ? ''
 | 
			
		||||
                                  : 'image/'
 | 
			
		||||
                              }${file.url.slice(1)}`}
 | 
			
		||||
                              target="_blank"
 | 
			
		||||
                            >
 | 
			
		||||
                              <Group gap={2}>
 | 
			
		||||
                                <IconDownload size={12} />
 | 
			
		||||
                                <Text size="xs">Download</Text>
 | 
			
		||||
                              </Group>
 | 
			
		||||
                            </Button>
 | 
			
		||||
                            <Button
 | 
			
		||||
                              size="xs"
 | 
			
		||||
                              variant="light"
 | 
			
		||||
                              color="red"
 | 
			
		||||
                              onClick={() => removeFile(file.id)}
 | 
			
		||||
                            >
 | 
			
		||||
                              <Group gap={2}>
 | 
			
		||||
                                <IconTrash size={12} />
 | 
			
		||||
                                <Text size="xs">Delete</Text>
 | 
			
		||||
                              </Group>
 | 
			
		||||
                            </Button>
 | 
			
		||||
                          </Group>
 | 
			
		||||
                        </Group>
 | 
			
		||||
                      </Card>
 | 
			
		||||
                    ),
 | 
			
		||||
                  )}
 | 
			
		||||
                </Stack>
 | 
			
		||||
              </Collapse>
 | 
			
		||||
            </Card>
 | 
			
		||||
          ))}
 | 
			
		||||
        </Stack>
 | 
			
		||||
 | 
			
		||||
        <Modal
 | 
			
		||||
          size={'lg'}
 | 
			
		||||
          size="lg"
 | 
			
		||||
          opened={openedProfile}
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setOpenedProfile(false)
 | 
			
		||||
            setCurrentUser('')
 | 
			
		||||
            setSelectedFile(null)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <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"
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={groupedFiles[currentUser] || []}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={currentUser}
 | 
			
		||||
            />
 | 
			
		||||
                <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>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,16 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  getFiles,
 | 
			
		||||
  getProfilesData,
 | 
			
		||||
  listUserTechnical,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
  updateProfilesData,
 | 
			
		||||
  updateUserTechnical,
 | 
			
		||||
  uploadFiles
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { changePassword } from '@/api/Auth'
 | 
			
		||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
			
		||||
import PasswordRequirementInput from '@/components/PasswordRequirementInput/PasswordRequirementInput'
 | 
			
		||||
import ProjectInvolvement from '@/components/ProjectInvolvement/ProjectInvolvement'
 | 
			
		||||
import { logout } from '@/rtk/dispatches/auth'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { get, post, postImage } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { requirementsPassword } from '@/rtk/helpers/variables'
 | 
			
		||||
import { useAppDispatch, useAppSelector } from '@/rtk/hooks'
 | 
			
		||||
| 
						 | 
				
			
			@ -22,16 +20,13 @@ import {
 | 
			
		|||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Flex,
 | 
			
		||||
  Group,
 | 
			
		||||
  Loader,
 | 
			
		||||
  Modal,
 | 
			
		||||
  PasswordInput,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Title,
 | 
			
		||||
  Title
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -46,15 +41,26 @@ import moment from 'moment'
 | 
			
		|||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import classes from './Profile.module.css'
 | 
			
		||||
import FileUploadForm from './components/FileUploadForm'
 | 
			
		||||
 | 
			
		||||
const isCompactMenu = false
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
// type TFileProfile = {
 | 
			
		||||
//   label: string
 | 
			
		||||
//   type: string
 | 
			
		||||
//   value: string
 | 
			
		||||
//   children?: TFileProfile[]
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
interface FileData {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  url: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  created_at: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Profile = () => {
 | 
			
		||||
  const user = useAppSelector((state) => state.authentication.user)
 | 
			
		||||
  const userData = getUser()
 | 
			
		||||
| 
						 | 
				
			
			@ -77,6 +83,11 @@ const Profile = () => {
 | 
			
		|||
  const navigate = useNavigate()
 | 
			
		||||
  const dispatch = useAppDispatch()
 | 
			
		||||
 | 
			
		||||
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
  const [data, setData] = useState<FileData[]>([])
 | 
			
		||||
  const [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const updateAvatar = async (file: File) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await postImage(updateProfilesData, file, 'post')
 | 
			
		||||
| 
						 | 
				
			
			@ -137,102 +148,113 @@ const Profile = () => {
 | 
			
		|||
    return []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [cv, setCv] = useState<File>()
 | 
			
		||||
  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 [openedProfile, setOpenedProfile] = useState(false)
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  // const [cv, setCv] = useState<File>()
 | 
			
		||||
  // const [idCard, setIdCard] = useState<File>()
 | 
			
		||||
  // const [transcript, setTranscript] = useState<File>()
 | 
			
		||||
  // const [universityDiploma, setUniversityDiploma] = useState<File>()
 | 
			
		||||
  // const [otherFiles, setOtherFiles] = useState([{ file: null, type: '' }])
 | 
			
		||||
  // const handleOtherFileChange = (
 | 
			
		||||
  //   index: number,
 | 
			
		||||
  //   field: string,
 | 
			
		||||
  //   value: File | string,
 | 
			
		||||
  // ) => {
 | 
			
		||||
  //   const updatedFiles: any = [...otherFiles]
 | 
			
		||||
  //   updatedFiles[index][field] = value
 | 
			
		||||
  //   setOtherFiles(updatedFiles)
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  // const addOtherFileInput = () => {
 | 
			
		||||
  //   setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (
 | 
			
		||||
    e: React.FormEvent,
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
      formData.append('file', selectedFile)
 | 
			
		||||
      formData.append('name', fileName)
 | 
			
		||||
      formData.append('description', description)
 | 
			
		||||
      formData.append('user_name', user.user.name)
 | 
			
		||||
      formData.append('user_id', user.user.id.toString())
 | 
			
		||||
 | 
			
		||||
    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, {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      if(response.status === 200){
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          await getAllFile()
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
			
		||||
      })
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
        const userFiles = res.data[user.user.name] || [];
 | 
			
		||||
        setData(userFiles);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể tải danh sách file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, {file_url: url}, getAllFile)
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFile();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      const result = await getListProfilesData()
 | 
			
		||||
      setDataProfile(result ?? [])
 | 
			
		||||
      await getAllFile()
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleChangePassword = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -306,23 +328,19 @@ const Profile = () => {
 | 
			
		|||
    dispatch(logout(navigate))
 | 
			
		||||
  }, [dispatch, navigate])
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
  // const handleChangeFileName = (e: File, newName: string): File => {
 | 
			
		||||
  //   const originalFile = e;
 | 
			
		||||
  //   const extend = originalFile.name.split('.')[1];
 | 
			
		||||
  //   const newFileName = `${newName}.${extend}`;
 | 
			
		||||
  //   return new File([originalFile], newFileName, {
 | 
			
		||||
  //     type: originalFile.type,
 | 
			
		||||
  //   });
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
  // const checkFileExist = (nameField: string) => {
 | 
			
		||||
  //   const file = data.find((f) => f.name.includes(nameField));
 | 
			
		||||
  //   return file;
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
| 
						 | 
				
			
			@ -578,162 +596,14 @@ const Profile = () => {
 | 
			
		|||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <form onSubmit={handleSubmit}>
 | 
			
		||||
              <Stack>
 | 
			
		||||
                <Box
 | 
			
		||||
                  style={{ display: checkFileExist('cv') ? 'flex' : 'none' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>CV</Text>
 | 
			
		||||
                  <a href={checkFileExist('cv')?.value} target="_blank">
 | 
			
		||||
                    <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"
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={data}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={user.user.name}
 | 
			
		||||
            />
 | 
			
		||||
                <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>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </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