truong-leave-day #116
			
				
			
		
		
		
	| 
						 | 
				
			
			@ -0,0 +1,161 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Modules\Admin\app\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use App\Http\Controllers\Controller;
 | 
			
		||||
use App\Traits\HasFilterRequest;
 | 
			
		||||
use App\Traits\HasOrderByRequest;
 | 
			
		||||
use App\Traits\HasSearchRequest;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Modules\Admin\app\Models\Document;
 | 
			
		||||
 | 
			
		||||
class DocumentController extends Controller
 | 
			
		||||
{
 | 
			
		||||
    use HasOrderByRequest;
 | 
			
		||||
    use HasFilterRequest;
 | 
			
		||||
    use HasSearchRequest;
 | 
			
		||||
    
 | 
			
		||||
    public function all(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $documents = new Document;
 | 
			
		||||
 | 
			
		||||
        // Order by
 | 
			
		||||
        $this->orderByRequest($documents, $request);
 | 
			
		||||
        $documents->orderBy('title', 'asc');
 | 
			
		||||
        // Filter
 | 
			
		||||
        $this->filterRequest(
 | 
			
		||||
            builder: $documents,
 | 
			
		||||
            request: $request,
 | 
			
		||||
            filterKeys: [
 | 
			
		||||
                'title' => self::F_TEXT,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
        $this->searchRequest(
 | 
			
		||||
            builder: $documents,
 | 
			
		||||
            value: $request->get('search'),
 | 
			
		||||
            fields: [
 | 
			
		||||
                'title',
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $responseData = $documents->get();
 | 
			
		||||
        return AbstractController::ResultSuccess($responseData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function create(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'type' => 'required|in:file,link',
 | 
			
		||||
            'files' => 'nullable|array',
 | 
			
		||||
            'files.*.title' => 'required|string|max:255',
 | 
			
		||||
            'files.*.file' => 'required|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
 | 
			
		||||
            'links' => 'nullable|array',
 | 
			
		||||
            'links.*.title' => 'required|string|max:255',
 | 
			
		||||
            'links.*.uri' => 'required|string|url',
 | 
			
		||||
            'is_active' => 'required|boolean',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $documents = [];
 | 
			
		||||
        
 | 
			
		||||
        if ($request->type === "file") {
 | 
			
		||||
            foreach ($request->file('files') as $index => $file) {
 | 
			
		||||
                $path = $file['file']->store('uploads', 'public');
 | 
			
		||||
                $documents[] = [
 | 
			
		||||
                    'title' => $request->input("files.$index.title"),
 | 
			
		||||
                    'type' => 'file',
 | 
			
		||||
                    'uri' => "storage/{$path}",
 | 
			
		||||
                    'is_active' => $request->is_active,
 | 
			
		||||
                    'created_at' => now(),
 | 
			
		||||
                    'updated_at' => now(),
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            foreach ($request->links as $link) {
 | 
			
		||||
                $documents[] = [
 | 
			
		||||
                    'title' => $link['title'],
 | 
			
		||||
                    'type' => 'link',
 | 
			
		||||
                    'uri' => $link['uri'],
 | 
			
		||||
                    'is_active' => $request->is_active,
 | 
			
		||||
                    'created_at' => now(),
 | 
			
		||||
                    'updated_at' => now(),
 | 
			
		||||
                ];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!empty($documents)) {
 | 
			
		||||
            Document::insert($documents);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return AbstractController::ResultSuccess($documents, "Documents created successfully!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function update(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'id' => 'required|exists:documents,id',
 | 
			
		||||
            'title' => 'required|string|max:255',
 | 
			
		||||
            'type' => 'required|in:file,link',
 | 
			
		||||
            'uri'   => 'nullable|url',
 | 
			
		||||
            'file' => 'nullable|file|mimes:doc,docx,xls,xlsx,pdf|max:20480',
 | 
			
		||||
            'existing_file' => 'nullable|string',
 | 
			
		||||
            'is_active' => 'required|boolean',
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $document = Document::find($request->input('id'));
 | 
			
		||||
        if (!$document) {
 | 
			
		||||
            return AbstractController::ResultError("Document not found.");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($request->type === "file") {
 | 
			
		||||
            $uri = $request->existing_file;
 | 
			
		||||
 | 
			
		||||
            if ($request->hasFile('file')) {
 | 
			
		||||
                $filePath = str_replace('storage/', 'public/', $request->existing_file);
 | 
			
		||||
                if (Storage::exists($filePath)) {
 | 
			
		||||
                    Storage::delete($filePath);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $path = $request->file('file')->store('uploads', 'public');
 | 
			
		||||
                $uri = "storage/{$path}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $document->update([
 | 
			
		||||
                'title' => $request->title,
 | 
			
		||||
                'type' => $request->type,
 | 
			
		||||
                'uri' => $uri,
 | 
			
		||||
                'is_active' => $request->is_active,
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            return AbstractController::ResultSuccess($document, "Document updated successfully!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $document->update([
 | 
			
		||||
            'title' => $request->title,
 | 
			
		||||
            'type' => $request->type,
 | 
			
		||||
            'uri' =>  $request->uri,
 | 
			
		||||
            'is_active' => $request->is_active,
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        return AbstractController::ResultSuccess($document, "Document updated successfully!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function delete(Request $request)
 | 
			
		||||
    {
 | 
			
		||||
        $id = $request->input('id');
 | 
			
		||||
        $document = Document::find($id);
 | 
			
		||||
        if (!$document) {
 | 
			
		||||
            return AbstractController::ResultError("Document not found");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($document->type === "file") {
 | 
			
		||||
            $filePath = str_replace('storage/', 'public/', $document->uri);
 | 
			
		||||
            if (Storage::exists($filePath)) {
 | 
			
		||||
                Storage::delete($filePath);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $document->delete();
 | 
			
		||||
        return AbstractController::ResultSuccess("Document deleted successfully!");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -178,6 +178,27 @@ class JiraController extends Controller
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getWeeklyReport()
 | 
			
		||||
    {
 | 
			
		||||
        try {
 | 
			
		||||
            $startOfWeek = Carbon::now()->startOfWeek()->format('Y-m-d'); // Mặc định là Thứ Hai
 | 
			
		||||
            $endOfWeek = Carbon::now()->endOfWeek()->format('Y-m-d'); // Mặc định là Chủ Nhật
 | 
			
		||||
            // dd($startOfWeek);
 | 
			
		||||
            $results = [];
 | 
			
		||||
            $workLogs = $this->jiraService->getAllUserWorkLogs($startOfWeek, $endOfWeek);
 | 
			
		||||
            foreach($workLogs as $data){
 | 
			
		||||
                $results[$data['username']] = $data['information']['issues'];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                $results
 | 
			
		||||
            ], 200);
 | 
			
		||||
        } catch (\Exception $e) {
 | 
			
		||||
            return response()->json(['error' => $e->getMessage()], 500);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function sendReport()
 | 
			
		||||
    {
 | 
			
		||||
        $dateFormatted = Carbon::yesterday()->setTimezone(env('TIME_ZONE'))->format('Y-m-d');
 | 
			
		||||
| 
						 | 
				
			
			@ -186,7 +207,7 @@ class JiraController extends Controller
 | 
			
		|||
 | 
			
		||||
        $tasksByUser = $this->formatWorkLogsByUser($workLogs);
 | 
			
		||||
        // Mail::to(['luanlt632000@gmail.com'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
        Mail::to(['luanlt632000@gmail.com', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
        Mail::to(['joseph@apactech.io', 'admin@apactech.io'])->send(new WorklogReport($tasksByUser));
 | 
			
		||||
 | 
			
		||||
        // return "Email sent successfully!";
 | 
			
		||||
        return response()->json([
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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]);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +175,7 @@ class ProfileController extends Controller
 | 
			
		|||
        $name = $request->input('name') ?? auth('admins')->user()->name;
 | 
			
		||||
        // Validate the incoming files
 | 
			
		||||
        $request->validate([
 | 
			
		||||
            'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx|max:5120', // Adjust file types and size limit as needed
 | 
			
		||||
            'files.*' => 'required|file|mimes:jpg,png,jpeg,pdf,doc,docx,xlsx,xls,csv|max:5120', // Adjust file types and size limit as needed
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $uploadedFiles = [];
 | 
			
		||||
| 
						 | 
				
			
			@ -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,201 @@ 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 {
 | 
			
		||||
            // Lấy tất cả users
 | 
			
		||||
            $users = Admin::all();
 | 
			
		||||
            
 | 
			
		||||
            // Lấy files và map theo cấu trúc
 | 
			
		||||
            $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
 | 
			
		||||
                    ];
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            // Tạo mảng kết quả với tất cả users, không có file thì mảng rỗng
 | 
			
		||||
            $result = $users->pluck('name')->mapWithKeys(function($userName) use ($files) {
 | 
			
		||||
                $userFiles = $files->where('user_name', $userName)
 | 
			
		||||
                    ->map(function($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']
 | 
			
		||||
                        ];
 | 
			
		||||
                    })->values();
 | 
			
		||||
                    
 | 
			
		||||
                return [$userName => $userFiles];
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return response()->json([
 | 
			
		||||
                'status' => true,
 | 
			
		||||
                'data' => $result
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        } 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';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Modules\Admin\app\Models;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
 | 
			
		||||
class Document extends Model
 | 
			
		||||
{
 | 
			
		||||
    use HasFactory;
 | 
			
		||||
 | 
			
		||||
    protected $fillable = ['title', 'uri', 'type', 'is_active'];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,6 +2,7 @@
 | 
			
		|||
 | 
			
		||||
use App\Http\Middleware\CheckPermission;
 | 
			
		||||
use Illuminate\Support\Facades\Route;
 | 
			
		||||
use Illuminate\Support\Facades\Storage;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\AdminController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\BannerController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\CategoryController;
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +10,7 @@ use Modules\Admin\app\Http\Controllers\ClientController;
 | 
			
		|||
use Modules\Admin\app\Http\Controllers\CountryController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\CustomThemeController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\DashboardController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\DocumentController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\JiraController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\LeaveManagementController;
 | 
			
		||||
use Modules\Admin\app\Http\Controllers\SettingController;
 | 
			
		||||
| 
						 | 
				
			
			@ -112,6 +114,8 @@ Route::middleware('api')
 | 
			
		|||
                Route::get('/get-all-sprint-by-id-board', [JiraController::class, 'getAllSprintByIdBoard'])->middleware('check.permission:admin.tester');
 | 
			
		||||
                Route::get('/get-all-issue-by-id-sprint', [JiraController::class, 'getAllIssueByIdSprint']);
 | 
			
		||||
 | 
			
		||||
                Route::get('/export-weekly-report', [JiraController::class, 'getWeeklyReport']);
 | 
			
		||||
 | 
			
		||||
                Route::get('/all-issue-by-project', [JiraController::class, 'fetchIssuesByProject']);
 | 
			
		||||
                Route::get('/worklogs', [JiraController::class, 'getAllUserWorkLogs'])->middleware('check.permission:admin.staff');
 | 
			
		||||
                Route::get('/allocation', [JiraController::class, 'getAllUserDoing'])->middleware('check.permission:admin.staff');
 | 
			
		||||
| 
						 | 
				
			
			@ -170,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([
 | 
			
		||||
| 
						 | 
				
			
			@ -214,6 +221,32 @@ Route::middleware('api')
 | 
			
		|||
                Route::get('/get-list-user-by-tech-id/{technicalId}', [TechnicalController::class, 'getListUserByTechnicalId'])->middleware('check.permission:admin');
 | 
			
		||||
                Route::post('/technicals-user/update', [TechnicalController::class, 'updateTechnicalsUser']);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::group([
 | 
			
		||||
                'prefix' => 'document',
 | 
			
		||||
            ], function () {
 | 
			
		||||
                Route::get('/all', [DocumentController::class, 'all'])->middleware('check.permission:admin.hr.staff.accountant');
 | 
			
		||||
                Route::post('/create', [DocumentController::class, 'create'])->middleware('check.permission:admin');
 | 
			
		||||
                Route::post('/update', [DocumentController::class, 'update'])->middleware('check.permission:admin');
 | 
			
		||||
                Route::get('/delete', [DocumentController::class, 'delete'])->middleware('check.permission:admin');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Route::get('/download-file/{filename}', function ($filename) {
 | 
			
		||||
                $path = "uploads/{$filename}";
 | 
			
		||||
 | 
			
		||||
                if (!Storage::disk('public')->exists($path)) {
 | 
			
		||||
                    return response()->json(['error' => 'File not found'], 404);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $headers = [
 | 
			
		||||
                    'Access-Control-Allow-Origin' => '*',
 | 
			
		||||
                    'Access-Control-Allow-Methods' => 'GET',
 | 
			
		||||
                    'Access-Control-Allow-Headers' => 'Content-Type',
 | 
			
		||||
                    'Content-Disposition' => 'inline; filename="' . $filename . '"',
 | 
			
		||||
                ];
 | 
			
		||||
 | 
			
		||||
                return response()->file(storage_path("app/public/{$path}"), $headers);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +23,9 @@ class JiraService
 | 
			
		|||
                'Authorization' => $this->authHeader,
 | 
			
		||||
                'Accept' => 'application/json',
 | 
			
		||||
                'Content-Type' => 'application/json'
 | 
			
		||||
            ]
 | 
			
		||||
            ],
 | 
			
		||||
            'timeout'  => 60, // Tăng thời gian timeout lên 60 giây
 | 
			
		||||
            'connect_timeout' => 30 // Tăng thời gian chờ kết nối lên 30 giây
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -253,7 +255,7 @@ class JiraService
 | 
			
		|||
 | 
			
		||||
            $issues = json_decode($response->getBody()->getContents(), true);
 | 
			
		||||
 | 
			
		||||
             // Lọc các issue không thuộc các project bị ignore
 | 
			
		||||
            // Lọc các issue không thuộc các project bị ignore
 | 
			
		||||
            $filtered_issues = array_filter($issues['issues'], function ($issue) use ($ignore_projects) {
 | 
			
		||||
                return !in_array($issue['fields']['project']['name'], $ignore_projects);
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			@ -266,7 +268,7 @@ class JiraService
 | 
			
		|||
 | 
			
		||||
            foreach ($issues['issues'] as $issue) {
 | 
			
		||||
                $projectName = $issue['fields']['project']['name'];
 | 
			
		||||
                if(!in_array($projectName, $ignore_projects)) {
 | 
			
		||||
                if (!in_array($projectName, $ignore_projects)) {
 | 
			
		||||
                    $username = $issue['fields']['assignee']['displayName'];
 | 
			
		||||
                    $issue['fields']['assignee']['emailAddress'] = $user['emailAddress'];
 | 
			
		||||
                    if (!isset($groupedIssues[$projectName])) {
 | 
			
		||||
| 
						 | 
				
			
			@ -288,7 +290,6 @@ class JiraService
 | 
			
		|||
                    $users_data[$user['displayName']]['total_spent'] = $users_data[$user['displayName']]['total_spent'] + $issue['fields']['timespent'];
 | 
			
		||||
                    $users_data[$user['displayName']]['total_est'] = $users_data[$user['displayName']]['total_est'] + ($issue['fields']['timeoriginalestimate'] ?? 0);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -325,4 +326,18 @@ class JiraService
 | 
			
		|||
        $response = $this->client->get('/rest/agile/1.0/sprint/' . $id . '/issue');
 | 
			
		||||
        return json_decode($response->getBody()->getContents(), true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getWeeklyReport()
 | 
			
		||||
    {
 | 
			
		||||
        $body = [
 | 
			
		||||
            'fields' => ['summary', 'status', 'timeoriginalestimate', 'timespent', 'assignee', 'project', 'worklog'],
 | 
			
		||||
            'jql' => 'worklogDate >= startOfWeek() AND worklogDate < startOfWeek(1) order by created DESC',
 | 
			
		||||
            'maxResults' => 1000
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        $response = $this->client->post('/rest/api/3/search', [
 | 
			
		||||
            'body' => json_encode($body)
 | 
			
		||||
        ]);
 | 
			
		||||
        return json_decode($response->getBody()->getContents(), true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\DB;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
return new class extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function up(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('documents', function (Blueprint $table) {
 | 
			
		||||
            $table->id();
 | 
			
		||||
            $table->string('title');
 | 
			
		||||
            $table->text('uri');
 | 
			
		||||
            $table->enum('type', ['file', 'link'])->default("file");
 | 
			
		||||
            $table->boolean('is_active')->default(true);
 | 
			
		||||
            $table->timestamps();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     */
 | 
			
		||||
    public function down(): void
 | 
			
		||||
    {
 | 
			
		||||
        Schema::dropIfExists('documents');
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -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> 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -34,6 +34,7 @@
 | 
			
		|||
    "react": "^18.2.0",
 | 
			
		||||
    "react-doc-viewer": "^0.1.14",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-file-viewer": "^1.2.1",
 | 
			
		||||
    "react-redux": "^8.1.3",
 | 
			
		||||
    "react-router-dom": "^6.19.0",
 | 
			
		||||
    "reactstrap": "^9.2.2",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,3 +110,17 @@ export const deleteFile = API_URL + 'v1/admin/profile/delete-profile-file'
 | 
			
		|||
export const listTechnical = API_URL + 'v1/admin/technical/get-all'
 | 
			
		||||
export const createTechnical = API_URL + 'v1/admin/technical/create'
 | 
			
		||||
export const deleteTechnical = API_URL + 'v1/admin/technical/delete'
 | 
			
		||||
 | 
			
		||||
// Document
 | 
			
		||||
export const listDocument = API_URL + 'v1/admin/document/all'
 | 
			
		||||
export const createDocument = API_URL + 'v1/admin/document/create'
 | 
			
		||||
export const updateDocument = API_URL + 'v1/admin/document/update'
 | 
			
		||||
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'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -250,12 +250,7 @@ export const DataTableAll = ({
 | 
			
		|||
    if (query !== '') {
 | 
			
		||||
      setTData(
 | 
			
		||||
        data.filter((obj) =>
 | 
			
		||||
          Object.values(obj).some(
 | 
			
		||||
            (value: any) =>
 | 
			
		||||
              value !== null &&
 | 
			
		||||
              value.toString().toLowerCase().includes(query.toLowerCase()),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
          Object.values(obj)?.find((c: any) => c.toString().normalize('NFC').toLowerCase().includes(query.normalize('NFC').toLowerCase())))
 | 
			
		||||
      )
 | 
			
		||||
    } else {
 | 
			
		||||
      if (pagination) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,6 +27,7 @@ import {
 | 
			
		|||
  IconCalendarClock,
 | 
			
		||||
  IconChartDots2,
 | 
			
		||||
  IconDevices,
 | 
			
		||||
  IconFileInvoice,
 | 
			
		||||
  IconFolders,
 | 
			
		||||
  IconLayoutSidebarLeftExpand,
 | 
			
		||||
  IconLayoutSidebarRightExpand,
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +42,7 @@ import {
 | 
			
		|||
  IconSun,
 | 
			
		||||
  IconTicket,
 | 
			
		||||
  IconUsersGroup,
 | 
			
		||||
  IconZoomExclamation
 | 
			
		||||
  IconZoomExclamation,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import { useLocation, useNavigate } from 'react-router-dom'
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +72,13 @@ const data = [
 | 
			
		|||
    permissions: 'admin,hr,staff,tester',
 | 
			
		||||
    group: 'staff',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    link: '/documents',
 | 
			
		||||
    label: 'Documents',
 | 
			
		||||
    icon: IconFileInvoice,
 | 
			
		||||
    permissions: 'admin,hr,staff,tester,accountant',
 | 
			
		||||
    group: 'staff',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    link: '/leave-management',
 | 
			
		||||
    label: 'Leave Management',
 | 
			
		||||
| 
						 | 
				
			
			@ -239,7 +247,11 @@ const Navbar = ({
 | 
			
		|||
  // })
 | 
			
		||||
 | 
			
		||||
  const group = [
 | 
			
		||||
    { name: 'staff', label: 'General', permissions: 'admin,hr,staff,tester,accountant' },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'staff',
 | 
			
		||||
      label: 'General',
 | 
			
		||||
      permissions: 'admin,hr,staff,tester,accountant',
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'admin', label: 'Admin', permissions: 'admin' },
 | 
			
		||||
    { name: 'other', label: 'Other', permissions: 'admin,hr' },
 | 
			
		||||
    { name: 'test', label: 'Test', permissions: 'admin,tester' },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,3 +3,8 @@ declare module '*.css'
 | 
			
		|||
declare module '@codemirror/lang-javascript'
 | 
			
		||||
 | 
			
		||||
declare const __VITE_BACKEND_URL__: string
 | 
			
		||||
 | 
			
		||||
declare module 'react-file-viewer' {
 | 
			
		||||
  const FileViewer: any
 | 
			
		||||
  export default FileViewer
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,498 +1,330 @@
 | 
			
		|||
import {
 | 
			
		||||
  deleteFile,
 | 
			
		||||
  getAllFilesInProfiles,
 | 
			
		||||
  updateProfileFolder,
 | 
			
		||||
} from '@/api/Admin'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { getFiles, uploadFiles } from '@/api/Admin'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { getAccessToken } from '@/rtk/localStorage'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Card,
 | 
			
		||||
  Collapse,
 | 
			
		||||
  Group,
 | 
			
		||||
  Modal,
 | 
			
		||||
  RenderTreeNodePayload,
 | 
			
		||||
  Stack,
 | 
			
		||||
  Text,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Tree,
 | 
			
		||||
  Title,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconChevronDown,
 | 
			
		||||
  IconDownload,
 | 
			
		||||
  IconFileTypeDocx,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFolder,
 | 
			
		||||
  IconFolderOpen,
 | 
			
		||||
  IconFolderX,
 | 
			
		||||
  IconListCheck,
 | 
			
		||||
  IconPhoto,
 | 
			
		||||
  IconSearch,
 | 
			
		||||
  IconTrash,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import FileUploadForm from '../Profile/components/FileUploadForm'
 | 
			
		||||
import classes from './AllProfiles.module.css'
 | 
			
		||||
 | 
			
		||||
interface FileIconProps {
 | 
			
		||||
interface FileData {
 | 
			
		||||
  id: number
 | 
			
		||||
  name: string
 | 
			
		||||
  isFolder: boolean
 | 
			
		||||
  expanded: boolean
 | 
			
		||||
  url: string
 | 
			
		||||
  type: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  created_at: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TFileProfile = {
 | 
			
		||||
  label: string
 | 
			
		||||
  type: string
 | 
			
		||||
  value: string
 | 
			
		||||
  children?: TFileProfile[]
 | 
			
		||||
interface GroupedFiles {
 | 
			
		||||
  [key: string]: FileData[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AllProfiles = () => {
 | 
			
		||||
  const [treeData, setTreeData] = useState([])
 | 
			
		||||
  const [cv, setCv] = useState<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 }>({})
 | 
			
		||||
 | 
			
		||||
    if (name.endsWith('.doc') || name.endsWith('.docx')) {
 | 
			
		||||
      return <IconFileTypeDocx size={14} />
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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} />
 | 
			
		||||
    )
 | 
			
		||||
  const toggleFolder = (userName: string) => {
 | 
			
		||||
    setExpandedFolders((prev) => ({
 | 
			
		||||
      ...prev,
 | 
			
		||||
      [userName]: !prev[userName],
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 getFileIcon = (type: string) => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'document':
 | 
			
		||||
        return <IconFileTypeDocx size={16} />
 | 
			
		||||
      case 'image':
 | 
			
		||||
        return <IconPhoto size={16} />
 | 
			
		||||
      default:
 | 
			
		||||
        return <IconFileTypePdf size={16} />
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOtherFileChange = (
 | 
			
		||||
    index: number,
 | 
			
		||||
    field: string,
 | 
			
		||||
    value: File | string,
 | 
			
		||||
  const handleSubmit = async (
 | 
			
		||||
    e: React.FormEvent,
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
    currentUser: string
 | 
			
		||||
  ) => {
 | 
			
		||||
    const updatedFiles: any = [...otherFiles]
 | 
			
		||||
    updatedFiles[index][field] = value
 | 
			
		||||
    setOtherFiles(updatedFiles)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addOtherFileInput = () => {
 | 
			
		||||
    setOtherFiles([...otherFiles, { file: null, type: '' }])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async (e: any) => {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    const formData = new FormData()
 | 
			
		||||
    if (selectedFile) {
 | 
			
		||||
      formData.append('file', selectedFile)
 | 
			
		||||
      formData.append('name', fileName)
 | 
			
		||||
      formData.append('description', description)
 | 
			
		||||
      formData.append('user_name', currentUser)
 | 
			
		||||
 | 
			
		||||
    // Append each selected file to FormData
 | 
			
		||||
    for (let i = 0; i < otherFiles.length; i++) {
 | 
			
		||||
      if (otherFiles[i].file !== null && otherFiles[i].type !== '') {
 | 
			
		||||
        formData.append(
 | 
			
		||||
          'files[]',
 | 
			
		||||
          handleChangeFileName(otherFiles[i].file!, `__${otherFiles[i].type}`)!,
 | 
			
		||||
        )
 | 
			
		||||
      try {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
          await getAllFiles()
 | 
			
		||||
          return true
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
        throw error
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    formData.append('name', currentName)
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
  const getAllFiles = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setGroupedFiles(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'multipart/form-data',
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        getTree()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFiles();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + currentName,
 | 
			
		||||
      })
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, { file_url: url }, getAllFile)
 | 
			
		||||
      getTree()
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const getTree = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles',
 | 
			
		||||
      })
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setTreeData(res.data)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getTree()
 | 
			
		||||
    getAllFiles()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [currentName])
 | 
			
		||||
  const filterFiles = (files: FileData[], searchTerm: string) => {
 | 
			
		||||
    return files.filter(
 | 
			
		||||
      (file) =>
 | 
			
		||||
        file.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
 | 
			
		||||
        (file.description &&
 | 
			
		||||
          file.description.toLowerCase().includes(searchTerm.toLowerCase())),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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"
 | 
			
		||||
                />
 | 
			
		||||
                <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>
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={groupedFiles[currentUser] || []}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={currentUser}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Modal>
 | 
			
		||||
      </Box>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
.title {
 | 
			
		||||
  background-color: light-dark(var(white), var(--mantine-color-dark-7));
 | 
			
		||||
  z-index: 100;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding: 0 var(--mantine-spacing-sm) var(--mantine-spacing-lg)
 | 
			
		||||
    var(--mantine-spacing-sm);
 | 
			
		||||
  border-bottom: solid rgba(201, 201, 201, 0.377) 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.optionIcon {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: space-evenly;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteIcon {
 | 
			
		||||
  color: red;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
  border-radius: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editIcon {
 | 
			
		||||
  color: rgb(9, 132, 132);
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  padding: 2px;
 | 
			
		||||
  border-radius: 25%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.editIcon:hover {
 | 
			
		||||
  background-color: rgba(203, 203, 203, 0.809);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.deleteIcon:hover {
 | 
			
		||||
  background-color: rgba(203, 203, 203, 0.809);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,346 @@
 | 
			
		|||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { get } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { deleteDocument, listDocument } from '@/api/Admin'
 | 
			
		||||
import { Xdelete } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '@/rtk/store'
 | 
			
		||||
 | 
			
		||||
import { Anchor, Box, Button, Dialog, Group, Loader, Text } from '@mantine/core'
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
import {
 | 
			
		||||
  IconEdit,
 | 
			
		||||
  IconFileTypeDoc,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFileTypeXls,
 | 
			
		||||
  IconLink,
 | 
			
		||||
  IconX,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import DataTableAll from '@/components/DataTable/DataTable'
 | 
			
		||||
import ModalFileDocument from './ModalFileDocument'
 | 
			
		||||
 | 
			
		||||
import classes from './Document.module.css'
 | 
			
		||||
import ModalAddDocument from './ModalAddDocument'
 | 
			
		||||
import ModalEditDocument from './ModalEditDocument'
 | 
			
		||||
 | 
			
		||||
interface TDocument {
 | 
			
		||||
  id: number
 | 
			
		||||
  title: string
 | 
			
		||||
  uri: string
 | 
			
		||||
  type: string
 | 
			
		||||
  is_active: boolean
 | 
			
		||||
  created_at: string
 | 
			
		||||
  updated_at: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequestPagination = {
 | 
			
		||||
  data: TDocument[]
 | 
			
		||||
  status: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Document = () => {
 | 
			
		||||
  const user = useSelector((state: RootState) => state.authentication)
 | 
			
		||||
 | 
			
		||||
  const [loader, setLoader] = useState<boolean>(false)
 | 
			
		||||
  const [rows, setRows] = useState<RequestPagination>({
 | 
			
		||||
    data: [],
 | 
			
		||||
    status: true,
 | 
			
		||||
  })
 | 
			
		||||
  const [selectDataRow, setSelectDataRow] = useState<any>({})
 | 
			
		||||
 | 
			
		||||
  const [openedModalAdd, { open: openModalAdd, close: closeModalAdd }] =
 | 
			
		||||
    useDisclosure(false)
 | 
			
		||||
  const [openedModalEdit, { open: openModalEdit, close: closeModalEdit }] =
 | 
			
		||||
    useDisclosure(false)
 | 
			
		||||
  const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
 | 
			
		||||
    useDisclosure(false)
 | 
			
		||||
 | 
			
		||||
  const [openedDialogDelete, setOpenedDialogDelete] = useState(false)
 | 
			
		||||
  const [deleteLoader, setDeleteLoader] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getAllData()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const getAllData = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoader(true)
 | 
			
		||||
      const params = {}
 | 
			
		||||
      const res = await get(listDocument, params)
 | 
			
		||||
      if (res.status) {
 | 
			
		||||
        setRows(res)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Error',
 | 
			
		||||
        message: error.message ?? error,
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoader(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleDelete = async (id: number | undefined) => {
 | 
			
		||||
    try {
 | 
			
		||||
      setDeleteLoader(true)
 | 
			
		||||
      await Xdelete(deleteDocument, { id: id }, getAllData)
 | 
			
		||||
      setSelectDataRow({})
 | 
			
		||||
      setOpenedDialogDelete(false)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    } finally {
 | 
			
		||||
      setDeleteLoader(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getFileTypeIcon = (row: TDocument) => {
 | 
			
		||||
    const uri = row?.uri
 | 
			
		||||
    if (!uri) return null
 | 
			
		||||
 | 
			
		||||
    const extension = uri.split('.').pop()?.toLowerCase()
 | 
			
		||||
 | 
			
		||||
    if (['doc'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
            import.meta.env.VITE_BACKEND_URL?.includes('localhost')
 | 
			
		||||
              ? ''
 | 
			
		||||
              : 'image/'
 | 
			
		||||
          }${uri}`}
 | 
			
		||||
          download={row.title}
 | 
			
		||||
          target="_self"
 | 
			
		||||
        >
 | 
			
		||||
          <IconFileTypeDoc style={{ color: '#1e62c1' }} />
 | 
			
		||||
        </a>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['xls', 'xlsx'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
            import.meta.env.VITE_BACKEND_URL?.includes('localhost')
 | 
			
		||||
              ? ''
 | 
			
		||||
              : 'image/'
 | 
			
		||||
          }${uri}`}
 | 
			
		||||
          download={row.title}
 | 
			
		||||
          target="_self"
 | 
			
		||||
        >
 | 
			
		||||
          <IconFileTypeXls style={{ color: '#0e864b' }} />
 | 
			
		||||
        </a>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['docx'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <IconFileTypeDoc
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            openModalFile()
 | 
			
		||||
            setSelectDataRow(row)
 | 
			
		||||
          }}
 | 
			
		||||
          style={{ color: '#1e62c1' }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <IconFileTypePdf
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          openModalFile()
 | 
			
		||||
          setSelectDataRow(row)
 | 
			
		||||
        }}
 | 
			
		||||
        style={{ color: '#ff1b0e' }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'title',
 | 
			
		||||
      size: '50%',
 | 
			
		||||
      header: 'Title',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'uri',
 | 
			
		||||
      size: '45%',
 | 
			
		||||
      header: 'URI',
 | 
			
		||||
      render: (row: TDocument) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Box>
 | 
			
		||||
            {row.type === 'file' ? (
 | 
			
		||||
              <Box
 | 
			
		||||
                w="fit-content"
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                title={`File .${row?.uri
 | 
			
		||||
                  .split('.')
 | 
			
		||||
                  .pop()
 | 
			
		||||
                  ?.toLowerCase()} detail`}
 | 
			
		||||
              >
 | 
			
		||||
                {getFileTypeIcon(row)}
 | 
			
		||||
              </Box>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <Anchor
 | 
			
		||||
                ta="start"
 | 
			
		||||
                href={row?.uri}
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                title={row?.uri}
 | 
			
		||||
              >
 | 
			
		||||
                <IconLink />
 | 
			
		||||
              </Anchor>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: '#',
 | 
			
		||||
      size: '5%',
 | 
			
		||||
      header: 'Action',
 | 
			
		||||
      render: (row: TDocument) => {
 | 
			
		||||
        if (!user.user.user.permission.includes('admin')) {
 | 
			
		||||
          return ''
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
          <Box className={classes.optionIcon}>
 | 
			
		||||
            <IconEdit
 | 
			
		||||
              className={classes.editIcon}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setSelectDataRow(row)
 | 
			
		||||
                openModalEdit()
 | 
			
		||||
              }}
 | 
			
		||||
              width={20}
 | 
			
		||||
              height={20}
 | 
			
		||||
            />
 | 
			
		||||
            <IconX
 | 
			
		||||
              className={classes.deleteIcon}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setOpenedDialogDelete(true)
 | 
			
		||||
                setSelectDataRow(row)
 | 
			
		||||
              }}
 | 
			
		||||
              width={20}
 | 
			
		||||
              height={20}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box>
 | 
			
		||||
      <div className={classes.title}>
 | 
			
		||||
        <h3>
 | 
			
		||||
          <Text>Admin/</Text>
 | 
			
		||||
          Documents
 | 
			
		||||
        </h3>
 | 
			
		||||
 | 
			
		||||
        {user.user.user.permission.includes('admin') ? (
 | 
			
		||||
          <Button onClick={() => openModalAdd()} disabled={loader}>
 | 
			
		||||
            + Add
 | 
			
		||||
          </Button>
 | 
			
		||||
        ) : (
 | 
			
		||||
          ''
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Box mt={'md'}>
 | 
			
		||||
        {loader ? (
 | 
			
		||||
          <Box ta={'center'}>
 | 
			
		||||
            <Loader size={40} mt={'15%'} />
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <DataTableAll
 | 
			
		||||
            data={rows.data}
 | 
			
		||||
            columns={columns}
 | 
			
		||||
            size=""
 | 
			
		||||
            searchInput
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      {openedModalFile && (
 | 
			
		||||
        <ModalFileDocument
 | 
			
		||||
          opened={openedModalFile}
 | 
			
		||||
          close={closeModalFile}
 | 
			
		||||
          selectDataRow={selectDataRow}
 | 
			
		||||
          setSelectDataRow={setSelectDataRow}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {openedModalAdd && (
 | 
			
		||||
        <ModalAddDocument
 | 
			
		||||
          opened={openedModalAdd}
 | 
			
		||||
          close={closeModalAdd}
 | 
			
		||||
          getAllData={getAllData}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {openedModalEdit && (
 | 
			
		||||
        <ModalEditDocument
 | 
			
		||||
          opened={openedModalEdit}
 | 
			
		||||
          close={closeModalEdit}
 | 
			
		||||
          selectDataRow={selectDataRow}
 | 
			
		||||
          setSelectDataRow={setSelectDataRow}
 | 
			
		||||
          getAllData={getAllData}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {openedDialogDelete && (
 | 
			
		||||
        <Dialog
 | 
			
		||||
          opened={openedDialogDelete}
 | 
			
		||||
          className={classes.dialog}
 | 
			
		||||
          withCloseButton
 | 
			
		||||
          onClose={() => {
 | 
			
		||||
            setSelectDataRow({})
 | 
			
		||||
            setOpenedDialogDelete(false)
 | 
			
		||||
          }}
 | 
			
		||||
          size="lg"
 | 
			
		||||
          radius="md"
 | 
			
		||||
          position={{ top: 30, right: 10 }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box
 | 
			
		||||
            className={classes.dialogText}
 | 
			
		||||
            size="sm"
 | 
			
		||||
            mb="xs"
 | 
			
		||||
            fw={500}
 | 
			
		||||
            pr={20}
 | 
			
		||||
          >
 | 
			
		||||
            <Text ta="center">
 | 
			
		||||
              Do you want to delete the document <b>{selectDataRow?.title}</b> ?
 | 
			
		||||
            </Text>
 | 
			
		||||
 | 
			
		||||
            <Group justify="center" m={10}>
 | 
			
		||||
              <Button
 | 
			
		||||
                fw={700}
 | 
			
		||||
                size="xs"
 | 
			
		||||
                variant="filled"
 | 
			
		||||
                color="red"
 | 
			
		||||
                onClick={async () => handleDelete(selectDataRow?.id)}
 | 
			
		||||
                disabled={deleteLoader}
 | 
			
		||||
              >
 | 
			
		||||
                Yes
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                fw={700}
 | 
			
		||||
                size="xs"
 | 
			
		||||
                variant="filled"
 | 
			
		||||
                color="gray"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setSelectDataRow({})
 | 
			
		||||
                  setOpenedDialogDelete(false)
 | 
			
		||||
                }}
 | 
			
		||||
                disabled={deleteLoader}
 | 
			
		||||
              >
 | 
			
		||||
                Cancel
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Group>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Document
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,254 @@
 | 
			
		|||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { useForm } from '@mantine/form'
 | 
			
		||||
import {
 | 
			
		||||
  Modal,
 | 
			
		||||
  Button,
 | 
			
		||||
  Text,
 | 
			
		||||
  Box,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Group,
 | 
			
		||||
  ActionIcon,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
import { IconPlus, IconTrash } from '@tabler/icons-react'
 | 
			
		||||
 | 
			
		||||
import { create } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { createDocument } from '@/api/Admin'
 | 
			
		||||
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
 | 
			
		||||
import { notifications } from '@mantine/notifications'
 | 
			
		||||
 | 
			
		||||
type MProps = {
 | 
			
		||||
  opened: boolean
 | 
			
		||||
  close: () => void
 | 
			
		||||
  getAllData: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ModalAddDocument = ({ opened, close, getAllData }: MProps) => {
 | 
			
		||||
  const [loadingSubmit, setLoadingSubmit] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    initialValues: {
 | 
			
		||||
      type: 'file',
 | 
			
		||||
      files: [] as { title: string; file: File }[],
 | 
			
		||||
      links: [] as { title: string; uri: string }[],
 | 
			
		||||
      is_active: true,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    form.reset()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleCreate = async (values: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (values.type === 'file' && values.files.length === 0) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Error',
 | 
			
		||||
          message: 'No files uploaded!!!',
 | 
			
		||||
          color: 'red',
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (values.type === 'link' && values.links.length === 0) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Error',
 | 
			
		||||
          message: 'No links provided!!!',
 | 
			
		||||
          color: 'red',
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setLoadingSubmit(true)
 | 
			
		||||
      const header = await getHeaderInfo()
 | 
			
		||||
      const formData = new FormData()
 | 
			
		||||
      header.headers['Content-Type'] = 'multipart/form-data'
 | 
			
		||||
 | 
			
		||||
      formData.append('type', values.type)
 | 
			
		||||
      formData.append('is_active', values.is_active ? '1' : '0')
 | 
			
		||||
 | 
			
		||||
      if (values.type === 'file') {
 | 
			
		||||
        values.files.forEach((item: any, index: number) => {
 | 
			
		||||
          formData.append(`files[${index}][title]`, item.title)
 | 
			
		||||
          formData.append(`files[${index}][file]`, item.file)
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        values.links.forEach((item: any, index: number) => {
 | 
			
		||||
          formData.append(`links[${index}][title]`, item.title)
 | 
			
		||||
          formData.append(`links[${index}][uri]`, item.uri)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const res = await create(createDocument, formData, getAllData, header)
 | 
			
		||||
      if (res === true) {
 | 
			
		||||
        resetForm()
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoadingSubmit(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const resetForm = () => {
 | 
			
		||||
    close()
 | 
			
		||||
    form.reset()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      opened={opened}
 | 
			
		||||
      onClose={resetForm}
 | 
			
		||||
      size="lg"
 | 
			
		||||
      title={
 | 
			
		||||
        <Text fw={700} fz={'lg'}>
 | 
			
		||||
          Add Document
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <form onSubmit={form.onSubmit(handleCreate)}>
 | 
			
		||||
        <Switch
 | 
			
		||||
          style={{ width: 'fit-content' }}
 | 
			
		||||
          label={form.values.type === 'file' ? 'Upload files' : 'Enter links'}
 | 
			
		||||
          checked={form.values.type === 'file'}
 | 
			
		||||
          onChange={(event) =>
 | 
			
		||||
            form.setFieldValue(
 | 
			
		||||
              'type',
 | 
			
		||||
              event.currentTarget.checked ? 'file' : 'link',
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          mb={'md'}
 | 
			
		||||
          disabled={loadingSubmit}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        {form.values.type === 'file' ? (
 | 
			
		||||
          <Box>
 | 
			
		||||
            <FileInput
 | 
			
		||||
              accept=".doc,.docx,.xls,.xlsx,.pdf"
 | 
			
		||||
              label="Upload Doc, Excel, PDF files"
 | 
			
		||||
              multiple
 | 
			
		||||
              mb="md"
 | 
			
		||||
              value={form.values.files.map((file) => file.file)}
 | 
			
		||||
              onChange={(files) => {
 | 
			
		||||
                if (files) {
 | 
			
		||||
                  const newFiles = files.map((file) => ({
 | 
			
		||||
                    title: file.name.split('.')[0],
 | 
			
		||||
                    file,
 | 
			
		||||
                  }))
 | 
			
		||||
                  form.setFieldValue('files', newFiles)
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              required
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {form.values.files.map((item, index) => (
 | 
			
		||||
              <Group key={index} mb={'md'}>
 | 
			
		||||
                <TextInput
 | 
			
		||||
                  w="50%"
 | 
			
		||||
                  label="Title"
 | 
			
		||||
                  value={form.values.files[index].title}
 | 
			
		||||
                  onChange={(event) =>
 | 
			
		||||
                    form.setFieldValue(
 | 
			
		||||
                      `files.${index}.title`,
 | 
			
		||||
                      event.currentTarget.value,
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                  maxLength={255}
 | 
			
		||||
                  disabled={loadingSubmit}
 | 
			
		||||
                  required
 | 
			
		||||
                />
 | 
			
		||||
                <Text mt={24} w="30%">
 | 
			
		||||
                  {item.file?.name}
 | 
			
		||||
                </Text>
 | 
			
		||||
              </Group>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Box>
 | 
			
		||||
            {form.values.links.map((_, index) => (
 | 
			
		||||
              <Group key={index} mb={'md'}>
 | 
			
		||||
                <TextInput
 | 
			
		||||
                  w="35%"
 | 
			
		||||
                  label="Title"
 | 
			
		||||
                  value={form.values.links[index].title}
 | 
			
		||||
                  onChange={(event) =>
 | 
			
		||||
                    form.setFieldValue(
 | 
			
		||||
                      `links.${index}.title`,
 | 
			
		||||
                      event.currentTarget.value,
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                  maxLength={255}
 | 
			
		||||
                  disabled={loadingSubmit}
 | 
			
		||||
                  required
 | 
			
		||||
                />
 | 
			
		||||
                <TextInput
 | 
			
		||||
                  w="50%"
 | 
			
		||||
                  label="URL"
 | 
			
		||||
                  value={form.values.links[index].uri}
 | 
			
		||||
                  onChange={(event) =>
 | 
			
		||||
                    form.setFieldValue(
 | 
			
		||||
                      `links.${index}.uri`,
 | 
			
		||||
                      event.currentTarget.value,
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                  disabled={loadingSubmit}
 | 
			
		||||
                  required
 | 
			
		||||
                />
 | 
			
		||||
                <ActionIcon
 | 
			
		||||
                  color="red"
 | 
			
		||||
                  variant="light"
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                    form.setFieldValue(
 | 
			
		||||
                      'links',
 | 
			
		||||
                      form.values.links.filter((_, i) => i !== index),
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                  disabled={loadingSubmit}
 | 
			
		||||
                  mt={24}
 | 
			
		||||
                >
 | 
			
		||||
                  <IconTrash size={16} />
 | 
			
		||||
                </ActionIcon>
 | 
			
		||||
              </Group>
 | 
			
		||||
            ))}
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
              leftSection={<IconPlus size={16} />}
 | 
			
		||||
              variant="light"
 | 
			
		||||
              onClick={() =>
 | 
			
		||||
                form.setFieldValue('links', [
 | 
			
		||||
                  ...form.values.links,
 | 
			
		||||
                  { title: '', uri: '' },
 | 
			
		||||
                ])
 | 
			
		||||
              }
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              mb="md"
 | 
			
		||||
            >
 | 
			
		||||
              Add Link
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          label="Is Active"
 | 
			
		||||
          mb={'md'}
 | 
			
		||||
          checked={form.values.is_active}
 | 
			
		||||
          onChange={(event) =>
 | 
			
		||||
            form.setFieldValue('is_active', event.currentTarget.checked)
 | 
			
		||||
          }
 | 
			
		||||
          disabled={loadingSubmit}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Box ta="center" mt="lg">
 | 
			
		||||
          <Button color="green" type="submit" loading={loadingSubmit}>
 | 
			
		||||
            Create
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </form>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ModalAddDocument
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,285 @@
 | 
			
		|||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { useForm } from '@mantine/form'
 | 
			
		||||
import {
 | 
			
		||||
  Modal,
 | 
			
		||||
  Button,
 | 
			
		||||
  Text,
 | 
			
		||||
  Box,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  FileInput,
 | 
			
		||||
  Badge,
 | 
			
		||||
  TextInput,
 | 
			
		||||
  Group,
 | 
			
		||||
} from '@mantine/core'
 | 
			
		||||
 | 
			
		||||
import { update } from '@/rtk/helpers/CRUD'
 | 
			
		||||
import { updateDocument } from '@/api/Admin'
 | 
			
		||||
import { getHeaderInfo } from '@/rtk/helpers/tokenCreator'
 | 
			
		||||
import {
 | 
			
		||||
  IconFileTypeDoc,
 | 
			
		||||
  IconFileTypePdf,
 | 
			
		||||
  IconFileTypeXls,
 | 
			
		||||
} from '@tabler/icons-react'
 | 
			
		||||
import ModalFileDocument from './ModalFileDocument'
 | 
			
		||||
import { useDisclosure } from '@mantine/hooks'
 | 
			
		||||
 | 
			
		||||
type MProps = {
 | 
			
		||||
  opened: boolean
 | 
			
		||||
  close: () => void
 | 
			
		||||
  selectDataRow: any
 | 
			
		||||
  setSelectDataRow: any
 | 
			
		||||
  getAllData: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ModalEditDocument = ({
 | 
			
		||||
  opened,
 | 
			
		||||
  close,
 | 
			
		||||
  selectDataRow,
 | 
			
		||||
  setSelectDataRow,
 | 
			
		||||
  getAllData,
 | 
			
		||||
}: MProps) => {
 | 
			
		||||
  const [loadingSubmit, setLoadingSubmit] = useState(false)
 | 
			
		||||
  const [selectDataFileRow, setSelectDataFileRow] = useState<any>({})
 | 
			
		||||
 | 
			
		||||
  const [openedModalFile, { open: openModalFile, close: closeModalFile }] =
 | 
			
		||||
    useDisclosure(false)
 | 
			
		||||
 | 
			
		||||
  const form = useForm({
 | 
			
		||||
    initialValues: {
 | 
			
		||||
      title: '',
 | 
			
		||||
      type: true,
 | 
			
		||||
      file: null as File | null,
 | 
			
		||||
      uri: '',
 | 
			
		||||
      is_active: true,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    form.setValues({
 | 
			
		||||
      title: selectDataRow?.title || '',
 | 
			
		||||
      type: selectDataRow?.type === 'file',
 | 
			
		||||
      uri: selectDataRow?.uri || '',
 | 
			
		||||
      is_active: selectDataRow?.is_active || true,
 | 
			
		||||
    })
 | 
			
		||||
  }, [selectDataRow])
 | 
			
		||||
 | 
			
		||||
  const handleUpdate = async (data: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoadingSubmit(true)
 | 
			
		||||
 | 
			
		||||
      let formData: any = {}
 | 
			
		||||
      const header = await getHeaderInfo()
 | 
			
		||||
 | 
			
		||||
      if (data.type) {
 | 
			
		||||
        header.headers['Content-Type'] = 'multipart/form-data'
 | 
			
		||||
        const tmpFormData = new FormData()
 | 
			
		||||
 | 
			
		||||
        tmpFormData.append('title', data.title)
 | 
			
		||||
        tmpFormData.append('type', 'file')
 | 
			
		||||
        tmpFormData.append('is_active', data.is_active ? '1' : '0')
 | 
			
		||||
        tmpFormData.append('existing_file', data.uri)
 | 
			
		||||
 | 
			
		||||
        if (data.file) {
 | 
			
		||||
          tmpFormData.append('file', data.file)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        formData = tmpFormData
 | 
			
		||||
      } else {
 | 
			
		||||
        formData = {
 | 
			
		||||
          title: data.title,
 | 
			
		||||
          type: 'link',
 | 
			
		||||
          uri: data.uri,
 | 
			
		||||
          is_active: data.is_active ? '1' : '0',
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const res = await update(
 | 
			
		||||
        updateDocument + `?id=${selectDataRow?.id}`,
 | 
			
		||||
        formData,
 | 
			
		||||
        getAllData,
 | 
			
		||||
        header,
 | 
			
		||||
      )
 | 
			
		||||
      if (res === true) {
 | 
			
		||||
        resetForm()
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoadingSubmit(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getFileTypeIcon = (uri: string) => {
 | 
			
		||||
    if (!uri) return null
 | 
			
		||||
 | 
			
		||||
    const extension = uri.split('.').pop()?.toLowerCase()
 | 
			
		||||
 | 
			
		||||
    if (['doc'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
 | 
			
		||||
          download="Document Download"
 | 
			
		||||
          target="_self"
 | 
			
		||||
        >
 | 
			
		||||
          <IconFileTypeDoc style={{ color: '#1e62c1' }} />
 | 
			
		||||
        </a>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['xls', 'xlsx'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${uri}`}
 | 
			
		||||
          download="Document Download"
 | 
			
		||||
          target="_self"
 | 
			
		||||
        >
 | 
			
		||||
          <IconFileTypeXls style={{ color: '#0e864b' }} />
 | 
			
		||||
        </a>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['docx'].includes(extension!)) {
 | 
			
		||||
      return (
 | 
			
		||||
        <IconFileTypeDoc
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            openModalFile()
 | 
			
		||||
            setSelectDataFileRow(selectDataRow)
 | 
			
		||||
          }}
 | 
			
		||||
          style={{ color: '#1e62c1' }}
 | 
			
		||||
        />
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <IconFileTypePdf
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          openModalFile()
 | 
			
		||||
          setSelectDataFileRow(selectDataRow)
 | 
			
		||||
        }}
 | 
			
		||||
        style={{ color: '#ff1b0e' }}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const resetForm = () => {
 | 
			
		||||
    close()
 | 
			
		||||
    form.reset()
 | 
			
		||||
    setSelectDataRow({})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      opened={opened}
 | 
			
		||||
      onClose={resetForm}
 | 
			
		||||
      size="lg"
 | 
			
		||||
      title={
 | 
			
		||||
        <Text fw={700} fz={'lg'}>
 | 
			
		||||
          Update Document
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <form
 | 
			
		||||
        onSubmit={form.onSubmit((values) => {
 | 
			
		||||
          handleUpdate(values)
 | 
			
		||||
        })}
 | 
			
		||||
      >
 | 
			
		||||
        {form.values.type ? (
 | 
			
		||||
          <Badge tt="initial" color="orange" mb="md">
 | 
			
		||||
            Files
 | 
			
		||||
          </Badge>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Badge tt="initial" mb="md">
 | 
			
		||||
            Links
 | 
			
		||||
          </Badge>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {form.values.type ? (
 | 
			
		||||
          <Box>
 | 
			
		||||
            <TextInput
 | 
			
		||||
              label="Title"
 | 
			
		||||
              key={'title'}
 | 
			
		||||
              {...form.getInputProps('title')}
 | 
			
		||||
              maxLength={255}
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              required
 | 
			
		||||
              mb={'md'}
 | 
			
		||||
            />
 | 
			
		||||
            <FileInput
 | 
			
		||||
              accept=".doc,.docx,.xls,.xlsx,.pdf"
 | 
			
		||||
              label="Upload Doc, Excel, PDF file"
 | 
			
		||||
              value={form.values.file}
 | 
			
		||||
              onChange={(file) => form.setFieldValue('file', file || null)}
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              mb={'md'}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {form.values.file ? (
 | 
			
		||||
              ''
 | 
			
		||||
            ) : (
 | 
			
		||||
              <Group
 | 
			
		||||
                w="fit-content"
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                mb={'md'}
 | 
			
		||||
                title={`File ${form.values.uri
 | 
			
		||||
                  .split('.')
 | 
			
		||||
                  .pop()
 | 
			
		||||
                  ?.toLowerCase()} detail`}
 | 
			
		||||
              >
 | 
			
		||||
                {getFileTypeIcon(form.values.uri)}
 | 
			
		||||
 | 
			
		||||
                <Text>.{form.values.uri.split('.').pop()?.toLowerCase()}</Text>
 | 
			
		||||
              </Group>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Box>
 | 
			
		||||
            <TextInput
 | 
			
		||||
              label="Title"
 | 
			
		||||
              key={'title'}
 | 
			
		||||
              {...form.getInputProps('title')}
 | 
			
		||||
              maxLength={255}
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              required
 | 
			
		||||
              mb={'md'}
 | 
			
		||||
            />
 | 
			
		||||
            <TextInput
 | 
			
		||||
              label="Enter URI"
 | 
			
		||||
              key={'uri'}
 | 
			
		||||
              {...form.getInputProps('uri')}
 | 
			
		||||
              disabled={loadingSubmit}
 | 
			
		||||
              required
 | 
			
		||||
              mb={'md'}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <Checkbox
 | 
			
		||||
          label="Is Active"
 | 
			
		||||
          mb={'md'}
 | 
			
		||||
          checked={form.values.is_active}
 | 
			
		||||
          onChange={(event) =>
 | 
			
		||||
            form.setFieldValue('is_active', event.currentTarget.checked)
 | 
			
		||||
          }
 | 
			
		||||
          disabled={loadingSubmit}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <Box ta="center" mt="lg">
 | 
			
		||||
          <Button color="green" type="submit" loading={loadingSubmit}>
 | 
			
		||||
            Update
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </form>
 | 
			
		||||
 | 
			
		||||
      {openedModalFile && (
 | 
			
		||||
        <ModalFileDocument
 | 
			
		||||
          opened={openedModalFile}
 | 
			
		||||
          close={closeModalFile}
 | 
			
		||||
          selectDataRow={selectDataFileRow}
 | 
			
		||||
          setSelectDataRow={setSelectDataFileRow}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ModalEditDocument
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,115 @@
 | 
			
		|||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { getDownloadFile } from '@/rtk/helpers/apiService'
 | 
			
		||||
import { Modal, Text, Box, Loader, Paper, Button, Group } from '@mantine/core'
 | 
			
		||||
import FileViewer from 'react-file-viewer'
 | 
			
		||||
import { IconDownload } from '@tabler/icons-react'
 | 
			
		||||
import { downloadFile } from '@/api/Admin'
 | 
			
		||||
 | 
			
		||||
type MProps = {
 | 
			
		||||
  opened: boolean
 | 
			
		||||
  close: () => void
 | 
			
		||||
  selectDataRow: any
 | 
			
		||||
  setSelectDataRow: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TDocumentFile {
 | 
			
		||||
  uri: string
 | 
			
		||||
  fileType: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ModalFileDocument = ({
 | 
			
		||||
  opened,
 | 
			
		||||
  close,
 | 
			
		||||
  selectDataRow,
 | 
			
		||||
  setSelectDataRow,
 | 
			
		||||
}: MProps) => {
 | 
			
		||||
  const [fileDoc, setFileDoc] = useState<TDocumentFile>()
 | 
			
		||||
  const [loader, setLoader] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getFile()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const getFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoader(true)
 | 
			
		||||
      const params = {}
 | 
			
		||||
      const fileUri = selectDataRow?.uri.replace('storage/uploads/', '')
 | 
			
		||||
      const res = await getDownloadFile(`${downloadFile}/${fileUri}`, params)
 | 
			
		||||
 | 
			
		||||
      setFileDoc({
 | 
			
		||||
        uri: URL.createObjectURL(res.data),
 | 
			
		||||
        fileType: getFileType(selectDataRow?.uri) || 'default',
 | 
			
		||||
      })
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoader(false)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const supportedFileTypes = ['pdf', 'xlsx', 'xls', 'docx', 'doc']
 | 
			
		||||
  const getFileType = (fileName: string) => {
 | 
			
		||||
    const extension = fileName.split('.').pop()?.toLowerCase()
 | 
			
		||||
 | 
			
		||||
    return supportedFileTypes.includes(extension!) ? extension : 'default'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      opened={opened}
 | 
			
		||||
      onClose={() => {
 | 
			
		||||
        close()
 | 
			
		||||
        setSelectDataRow({})
 | 
			
		||||
      }}
 | 
			
		||||
      size="65%"
 | 
			
		||||
      title={
 | 
			
		||||
        <Text fw={700} fz={'lg'}>
 | 
			
		||||
          File Detail: {selectDataRow?.title}
 | 
			
		||||
        </Text>
 | 
			
		||||
      }
 | 
			
		||||
    >
 | 
			
		||||
      <Group justify="flex-end" mb={'md'}>
 | 
			
		||||
        <a
 | 
			
		||||
          href={`${import.meta.env.VITE_BACKEND_URL}${
 | 
			
		||||
            import.meta.env.VITE_BACKEND_URL?.includes('localhost')
 | 
			
		||||
              ? ''
 | 
			
		||||
              : 'image/'
 | 
			
		||||
          }${selectDataRow.uri}`}
 | 
			
		||||
          download="Document Download"
 | 
			
		||||
          target={
 | 
			
		||||
            getFileType(selectDataRow?.uri) === 'pdf' ? '_blank' : '_self'
 | 
			
		||||
          }
 | 
			
		||||
        >
 | 
			
		||||
          <Button
 | 
			
		||||
            leftSection={<IconDownload size={18} />}
 | 
			
		||||
            color={
 | 
			
		||||
              getFileType(selectDataRow?.uri) === 'pdf'
 | 
			
		||||
                ? 'red'
 | 
			
		||||
                : getFileType(selectDataRow?.uri) === 'xlsx' ||
 | 
			
		||||
                  getFileType(selectDataRow?.uri) === 'xls'
 | 
			
		||||
                ? 'green'
 | 
			
		||||
                : ''
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
            Download .{getFileType(selectDataRow?.uri)}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </a>
 | 
			
		||||
      </Group>
 | 
			
		||||
 | 
			
		||||
      <Paper withBorder>
 | 
			
		||||
        {loader ? (
 | 
			
		||||
          <Box ta={'center'} my={20}>
 | 
			
		||||
            <Loader size={40} />
 | 
			
		||||
          </Box>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Box w="100%">
 | 
			
		||||
            <FileViewer fileType={fileDoc?.fileType} filePath={fileDoc?.uri} />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
      </Paper>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ModalFileDocument
 | 
			
		||||
| 
						 | 
				
			
			@ -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())
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const token = await getAccessToken()
 | 
			
		||||
        const response = await axios.post(uploadFiles, formData, {
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'multipart/form-data',
 | 
			
		||||
            Authorization: `Bearer ${token}`,
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          await getAllFile()
 | 
			
		||||
          setSelectedFile(null)
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Error uploading file:', error)
 | 
			
		||||
      } finally {
 | 
			
		||||
        setIsLoading(false)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (cv) {
 | 
			
		||||
      formData.append('files[]', cv)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (idCard) {
 | 
			
		||||
      formData.append('files[]', idCard)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transcript) {
 | 
			
		||||
      formData.append('files[]', transcript)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (universityDiploma) {
 | 
			
		||||
      formData.append('files[]', universityDiploma)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const token = await getAccessToken()
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.post(updateProfileFolder, formData, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'multipart/form-data',
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      
 | 
			
		||||
      if(response.status === 200){
 | 
			
		||||
        getAllFile()
 | 
			
		||||
        setOtherFiles([])
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('Error uploading files', error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAllFile = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await get(getAllFilesInProfiles, {
 | 
			
		||||
        root_folder: '/storage/profiles/' + JSON.parse(getUser())?.user?.name,
 | 
			
		||||
      })
 | 
			
		||||
      const res = await get(getFiles)
 | 
			
		||||
      if (res.status === true) {
 | 
			
		||||
        setData(res.data)
 | 
			
		||||
        const userFiles = res.data[user.user.name] || [];
 | 
			
		||||
        setData(userFiles);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể tải danh sách file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeFile = async (url: string) => {
 | 
			
		||||
  const removeFile = async (id: number) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Xdelete(deleteFile, {file_url: url}, getAllFile)
 | 
			
		||||
      const token = await getAccessToken();
 | 
			
		||||
      const response = await axios.delete(`${import.meta.env.VITE_BACKEND_URL}api/v1/admin/profile/files/${id}`, {
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: `Bearer ${token}`,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (response.status === 200) {
 | 
			
		||||
        notifications.show({
 | 
			
		||||
          title: 'Thành công',
 | 
			
		||||
          message: 'Xóa file thành công',
 | 
			
		||||
          color: 'green',
 | 
			
		||||
        });
 | 
			
		||||
        await getAllFile();
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
      console.log(error);
 | 
			
		||||
      notifications.show({
 | 
			
		||||
        title: 'Lỗi',
 | 
			
		||||
        message: 'Không thể xóa file',
 | 
			
		||||
        color: 'red',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const fetchData = async () => {
 | 
			
		||||
      const result = await getListProfilesData()
 | 
			
		||||
      setDataProfile(result ?? [])
 | 
			
		||||
      await getAllFile()
 | 
			
		||||
    }
 | 
			
		||||
    fetchData()
 | 
			
		||||
    getAllFile()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleChangePassword = async () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -306,23 +328,19 @@ const Profile = () => {
 | 
			
		|||
    dispatch(logout(navigate))
 | 
			
		||||
  }, [dispatch, navigate])
 | 
			
		||||
 | 
			
		||||
  const handleChangeFileName = (e: File, newName: string) => {
 | 
			
		||||
    const originalFile = e // Get the original file
 | 
			
		||||
    const extend = originalFile.name.split('.')[1]
 | 
			
		||||
    if (originalFile) {
 | 
			
		||||
      const newFileName = `${newName}.${extend}` // Create new file name
 | 
			
		||||
      const newFile = new File([originalFile], newFileName, {
 | 
			
		||||
        type: originalFile.type,
 | 
			
		||||
      }) // Create new file object
 | 
			
		||||
  // const handleChangeFileName = (e: File, newName: string): File => {
 | 
			
		||||
  //   const originalFile = e;
 | 
			
		||||
  //   const extend = originalFile.name.split('.')[1];
 | 
			
		||||
  //   const newFileName = `${newName}.${extend}`;
 | 
			
		||||
  //   return new File([originalFile], newFileName, {
 | 
			
		||||
  //     type: originalFile.type,
 | 
			
		||||
  //   });
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
      return newFile // Save the new file object for further processing
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkFileExist = (nameField: string) => {
 | 
			
		||||
    const file = data.find((f) => f.label.includes(nameField))
 | 
			
		||||
    return file
 | 
			
		||||
  }
 | 
			
		||||
  // const checkFileExist = (nameField: string) => {
 | 
			
		||||
  //   const file = data.find((f) => f.name.includes(nameField));
 | 
			
		||||
  //   return file;
 | 
			
		||||
  // };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <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"
 | 
			
		||||
                />
 | 
			
		||||
                <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>
 | 
			
		||||
            <FileUploadForm
 | 
			
		||||
              data={data}
 | 
			
		||||
              handleSubmit={handleSubmit}
 | 
			
		||||
              handleFileChange={(file) => file && setSelectedFile(file)}
 | 
			
		||||
              removeFile={removeFile}
 | 
			
		||||
              isLoading={isLoading}
 | 
			
		||||
              currentUser={user.user.name}
 | 
			
		||||
            />
 | 
			
		||||
          </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
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,7 @@ import ProtectedRoute from '@/components/ProtectedRoute/ProtectedRoute'
 | 
			
		|||
import AllProfiles from '@/pages/AllProfiles/AllProfiles'
 | 
			
		||||
import Allocation from '@/pages/Allocation/Allocation'
 | 
			
		||||
import PageLogin from '@/pages/Auth/Login/Login'
 | 
			
		||||
import Document from '@/pages/Document/Document'
 | 
			
		||||
import LeaveManagement from '@/pages/LeaveManagement/LeaveManagement'
 | 
			
		||||
import PageNotFound from '@/pages/NotFound/NotFound'
 | 
			
		||||
import OrganizationSettings from '@/pages/OrganizationSettings/OrganizationSettings'
 | 
			
		||||
| 
						 | 
				
			
			@ -81,6 +82,20 @@ const mainRoutes = [
 | 
			
		|||
      </ProtectedRoute>
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/documents',
 | 
			
		||||
    element: (
 | 
			
		||||
      <ProtectedRoute mode="home" permission="staff,accountant">
 | 
			
		||||
        <BasePage
 | 
			
		||||
          main={
 | 
			
		||||
            <>
 | 
			
		||||
              <Document />
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
        ></BasePage>
 | 
			
		||||
      </ProtectedRoute>
 | 
			
		||||
    ),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/timekeeping',
 | 
			
		||||
    element: (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue